From aad29295e1b4a5fda335ddfa4098da504c915bca Mon Sep 17 00:00:00 2001 From: George Pollard Date: Fri, 9 Dec 2022 10:31:40 +1300 Subject: [PATCH] Repro Create should fail if insert fails, add tests (#2678) At the moment the result of the insert is ignored. --- .../ApiService/onefuzzlib/ReproOperations.cs | 47 ++-- .../ApiService/onefuzzlib/Request.cs | 8 +- .../IntegrationTests/Fakes/TestContext.cs | 8 +- .../IntegrationTests/ReproVmssTests.cs | 249 ++++++++++++++++++ 4 files changed, 283 insertions(+), 29 deletions(-) create mode 100644 src/ApiService/IntegrationTests/ReproVmssTests.cs diff --git a/src/ApiService/ApiService/onefuzzlib/ReproOperations.cs b/src/ApiService/ApiService/onefuzzlib/ReproOperations.cs index 382e6c87a..98e065c08 100644 --- a/src/ApiService/ApiService/onefuzzlib/ReproOperations.cs +++ b/src/ApiService/ApiService/onefuzzlib/ReproOperations.cs @@ -327,30 +327,33 @@ public class ReproOperations : StatefulOrm, IRe public async Task> Create(ReproConfig config, UserInfo userInfo) { var reportOrRegression = await _context.Reports.GetReportOrRegression(config.Container, config.Path); - if (reportOrRegression is Report report) { - var task = await _context.TaskOperations.GetByTaskId(report.TaskId); - if (task == null) { - return OneFuzzResult.Error(ErrorCode.INVALID_REQUEST, "unable to find task"); - } - - var vm = new Repro( - VmId: Guid.NewGuid(), - Config: config, - TaskId: task.TaskId, - Os: task.Os, - Auth: await Auth.BuildAuth(_logTracer), - EndTime: DateTimeOffset.UtcNow + TimeSpan.FromHours(config.Duration), - UserInfo: userInfo - ); - - var r = await _context.ReproOperations.Insert(vm); - if (!r.IsOk) { - _logTracer.WithHttpStatus(r.ErrorV).Error($"failed to insert repro record for {vm.VmId:Tag:VmId}"); - } - return OneFuzzResult.Ok(vm); - } else { + if (reportOrRegression is not Report report) { return OneFuzzResult.Error(ErrorCode.UNABLE_TO_FIND, "unable to find report"); } + + var task = await _context.TaskOperations.GetByTaskId(report.TaskId); + if (task is null) { + return OneFuzzResult.Error(ErrorCode.INVALID_REQUEST, "unable to find task"); + } + + var vm = new Repro( + VmId: Guid.NewGuid(), + Config: config, + TaskId: task.TaskId, + Os: task.Os, + Auth: await Auth.BuildAuth(_logTracer), + EndTime: DateTimeOffset.UtcNow + TimeSpan.FromHours(config.Duration), + UserInfo: userInfo); + + var r = await _context.ReproOperations.Insert(vm); + if (!r.IsOk) { + _logTracer.WithHttpStatus(r.ErrorV).Error($"failed to insert repro record for {vm.VmId:Tag:VmId}"); + return OneFuzzResult.Error( + ErrorCode.UNABLE_TO_CREATE, + new[] { "failed to insert repro record" }); + } + + return OneFuzzResult.Ok(vm); } public Task ExtensionsFailed(Repro repro) { diff --git a/src/ApiService/ApiService/onefuzzlib/Request.cs b/src/ApiService/ApiService/onefuzzlib/Request.cs index d4c159c48..4e19d32f8 100644 --- a/src/ApiService/ApiService/onefuzzlib/Request.cs +++ b/src/ApiService/ApiService/onefuzzlib/Request.cs @@ -14,7 +14,7 @@ public interface IRequestHandling { } // See: https://www.rfc-editor.org/rfc/rfc7807#section-3 -public sealed class ProblemDetails { +public sealed record ProblemDetails { [JsonConstructor] public ProblemDetails(int status, string title, string detail) { Status = status; @@ -45,14 +45,14 @@ public sealed class ProblemDetails { /// change from occurrence to occurrence of the problem, except for purposes /// of localization (e.g., using proactive content negotiation; see /// [RFC7231], Section 3.4). - public string Title { get; set; } + public string Title { get; } /// The HTTP status code ([RFC7231], Section 6) generated by the origin /// server for this occurrence of the problem. - public int Status { get; set; } + public int Status { get; } // A human-readable explanation specific to this occurrence of the problem. - public string Detail { get; set; } + public string Detail { get; } } public class RequestHandling : IRequestHandling { diff --git a/src/ApiService/IntegrationTests/Fakes/TestContext.cs b/src/ApiService/IntegrationTests/Fakes/TestContext.cs index 9106dfc0d..79ff38d2f 100644 --- a/src/ApiService/IntegrationTests/Fakes/TestContext.cs +++ b/src/ApiService/IntegrationTests/Fakes/TestContext.cs @@ -36,6 +36,8 @@ public sealed class TestContext : IOnefuzzContext { ConfigOperations = new ConfigOperations(logTracer, this, cache); PoolOperations = new PoolOperations(logTracer, this); ScalesetOperations = new ScalesetOperations(logTracer, this); + ReproOperations = new ReproOperations(logTracer, this); + Reports = new Reports(logTracer, Containers); UserCredentials = new UserCredentials(logTracer, ConfigOperations); } @@ -49,6 +51,7 @@ public sealed class TestContext : IOnefuzzContext { Node n => NodeOperations.Insert(n), Pool p => PoolOperations.Insert(p), Job j => JobOperations.Insert(j), + Repro r => ReproOperations.Insert(r), NodeTasks nt => NodeTasksOperations.Insert(nt), InstanceConfig ic => ConfigOperations.Insert(ic), _ => throw new NotSupportedException($"You will need to add an TestContext.InsertAll case for {x.GetType()} entities"), @@ -78,6 +81,8 @@ public sealed class TestContext : IOnefuzzContext { public IPoolOperations PoolOperations { get; } public IScalesetOperations ScalesetOperations { get; } public IVmssOperations VmssOperations { get; } + public IReproOperations ReproOperations { get; } + public IReports Reports { get; } public EntityConverter EntityConverter { get; } // -- Remainder not implemented -- @@ -100,9 +105,6 @@ public sealed class TestContext : IOnefuzzContext { public IProxyOperations ProxyOperations => throw new System.NotImplementedException(); - public IReports Reports => throw new System.NotImplementedException(); - - public IReproOperations ReproOperations => throw new System.NotImplementedException(); public IScheduler Scheduler => throw new System.NotImplementedException(); diff --git a/src/ApiService/IntegrationTests/ReproVmssTests.cs b/src/ApiService/IntegrationTests/ReproVmssTests.cs new file mode 100644 index 000000000..65b1834ab --- /dev/null +++ b/src/ApiService/IntegrationTests/ReproVmssTests.cs @@ -0,0 +1,249 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text.Json; +using IntegrationTests.Fakes; +using Microsoft.OneFuzz.Service; +using Microsoft.OneFuzz.Service.Functions; +using Microsoft.OneFuzz.Service.OneFuzzLib.Orm; +using Xunit; +using Xunit.Abstractions; +using Async = System.Threading.Tasks; + +namespace IntegrationTests.Functions; + +[Trait("Category", "Live")] +public class AzureStorageReproVmssTest : ReproVmssTestBase { + public AzureStorageReproVmssTest(ITestOutputHelper output) + : base(output, Integration.AzureStorage.FromEnvironment()) { } +} + +public class AzuriteReproVmssTest : ReproVmssTestBase { + public AzuriteReproVmssTest(ITestOutputHelper output) + : base(output, new Integration.AzuriteStorage()) { } +} + +public abstract class ReproVmssTestBase : FunctionTestBase { + public ReproVmssTestBase(ITestOutputHelper output, IStorage storage) + : base(output, storage) { } + + + [Theory] + [InlineData("POST", RequestType.Agent)] + [InlineData("POST", RequestType.NoAuthorization)] + [InlineData("GET", RequestType.Agent)] + [InlineData("GET", RequestType.NoAuthorization)] + [InlineData("DELETE", RequestType.Agent)] + [InlineData("DELETE", RequestType.NoAuthorization)] + public async Async.Task UserAuthorization_IsRequired(string method, RequestType authType) { + var auth = new TestEndpointAuthorization(authType, Logger, Context); + var func = new ReproVmss(Logger, auth, Context); + var result = await func.Run(TestHttpRequestData.Empty(method)); + Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode); + } + + [Fact] + public async Async.Task GetMissingVmFails() { + var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context); + var func = new ReproVmss(Logger, auth, Context); + var req = new ReproGet(VmId: Guid.NewGuid()); + var result = await func.Run(TestHttpRequestData.FromJson("GET", req)); + // TODO: should this be 404? + Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); + var err = BodyAs(result); + Assert.Equal("no such VM", err.Detail); + } + + [Fact] + public async Async.Task GetAvailableVMsCanReturnEmpty() { + var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context); + var func = new ReproVmss(Logger, auth, Context); + var req = new ReproGet(VmId: null); // this means "all available" + var result = await func.Run(TestHttpRequestData.FromJson("GET", req)); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.Empty(BodyAs(result)); + } + + [Fact] + public async Async.Task GetAvailableVMsCanReturnVM() { + var vmId = Guid.NewGuid(); + + await Context.InsertAll( + new Repro( + VmId: vmId, + TaskId: Guid.NewGuid(), + new ReproConfig(Container.Parse("abcd"), "", 12345), + Auth: null, + Os: Os.Linux)); + + var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context); + var func = new ReproVmss(Logger, auth, Context); + var req = new ReproGet(VmId: null); // this means "all available" + var result = await func.Run(TestHttpRequestData.FromJson("GET", req)); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + var repro = Assert.Single(BodyAs(result)); + Assert.Equal(vmId, repro.VmId); + } + + [Fact] + public async Async.Task GetAvailableVMsCanReturnSpecificVM() { + var vmId = Guid.NewGuid(); + + await Context.InsertAll( + new Repro( + VmId: vmId, + TaskId: Guid.NewGuid(), + new ReproConfig(Container.Parse("abcd"), "", 12345), + Auth: null, + Os: Os.Linux)); + + var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context); + var func = new ReproVmss(Logger, auth, Context); + var req = new ReproGet(VmId: vmId); + var result = await func.Run(TestHttpRequestData.FromJson("GET", req)); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.Equal(vmId, BodyAs(result).VmId); + } + + + [Fact] + public async Async.Task GetAvailableVMsDoesNotReturnUnavailableVMs() { + await Context.InsertAll( + new Repro( + VmId: Guid.NewGuid(), + TaskId: Guid.NewGuid(), + new ReproConfig(Container.Parse("abcd"), "", 12345), + Auth: null, + Os: Os.Linux, + State: VmState.Stopping), + new Repro( + VmId: Guid.NewGuid(), + TaskId: Guid.NewGuid(), + new ReproConfig(Container.Parse("abcd"), "", 12345), + Auth: null, + Os: Os.Linux, + State: VmState.Stopped)); + + var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context); + var func = new ReproVmss(Logger, auth, Context); + var req = new ReproGet(VmId: null); // this means "all available" + var result = await func.Run(TestHttpRequestData.FromJson("GET", req)); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.Empty(BodyAs(result)); + } + + [Fact] + public async Async.Task CannotCreateVMWithoutCredentials() { + var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context); + + var func = new ReproVmss(Logger, auth, Context); + var req = new ReproCreate(Container.Parse("abcd"), "/", 12345); + var result = await func.Run(TestHttpRequestData.FromJson("POST", req)); + Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); + var err = BodyAs(result); + Assert.Equal(new ProblemDetails(400, "INVALID_REQUEST", "unable to find authorization token"), err); + } + + [Fact] + public async Async.Task CannotCreateVMForMissingReport() { + var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context); + + // setup fake user + var userInfo = new UserInfo(Guid.NewGuid(), Guid.NewGuid(), "upn"); + Context.UserCredentials = new TestUserCredentials(Logger, Context.ConfigOperations, OneFuzzResult.Ok(userInfo)); + + var func = new ReproVmss(Logger, auth, Context); + var req = new ReproCreate(Container.Parse("abcd"), "/", 12345); + var result = await func.Run(TestHttpRequestData.FromJson("POST", req)); + Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); + var err = BodyAs(result); + Assert.Equal(new ProblemDetails(400, "UNABLE_TO_FIND", "unable to find report"), err); + } + + private async Async.Task<(Container, string)> CreateContainerWithReport(Guid jobId, Guid taskId) { + var container = Container.Parse(Guid.NewGuid().ToString("N")); + var filename = "report.json"; + // Setup container with Report + var cc = GetContainerClient(container); + _ = await cc.CreateIfNotExistsAsync(); + using (var ms = new MemoryStream()) { + var emptyReport = new Report( + null, + null, + "", + "", + "", + new List(), + "", + "", + null, + TaskId: taskId, + JobId: jobId, + null, + null, + null, + null, + null, + null, + null, + null); + + JsonSerializer.Serialize(ms, emptyReport, EntityConverter.GetJsonSerializerOptions()); + _ = ms.Seek(0, SeekOrigin.Begin); + _ = await cc.UploadBlobAsync(filename, ms); + } + + return (container, filename); + } + + [Fact] + public async Async.Task CannotCreateVMForMissingTask() { + var (container, filename) = await CreateContainerWithReport(Guid.NewGuid(), Guid.NewGuid()); + + var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context); + + // setup fake user + var userInfo = new UserInfo(Guid.NewGuid(), Guid.NewGuid(), "upn"); + Context.UserCredentials = new TestUserCredentials(Logger, Context.ConfigOperations, OneFuzzResult.Ok(userInfo)); + + var func = new ReproVmss(Logger, auth, Context); + var req = new ReproCreate(container, filename, 12345); + var result = await func.Run(TestHttpRequestData.FromJson("POST", req)); + Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); + var err = BodyAs(result); + Assert.Equal(new ProblemDetails(400, "INVALID_REQUEST", "unable to find task"), err); + } + + [Fact] + public async Async.Task CanCreateVMSuccessfully() { + // report must have TaskID pointing to a valid Task + + var jobId = Guid.NewGuid(); + var taskId = Guid.NewGuid(); + var (container, filename) = await CreateContainerWithReport(jobId: jobId, taskId: taskId); + await Context.InsertAll( + new Task( + JobId: jobId, + TaskId: taskId, + TaskState.Running, + Os.Linux, + new TaskConfig( + JobId: jobId, + null, + new TaskDetails(TaskType.LibfuzzerFuzz, 12345)))); + + var auth = new TestEndpointAuthorization(RequestType.User, Logger, Context); + + // setup fake user + var userInfo = new UserInfo(Guid.NewGuid(), Guid.NewGuid(), "upn"); + Context.UserCredentials = new TestUserCredentials(Logger, Context.ConfigOperations, OneFuzzResult.Ok(userInfo)); + + var func = new ReproVmss(Logger, auth, Context); + var req = new ReproCreate(container, filename, 12345); + var result = await func.Run(TestHttpRequestData.FromJson("POST", req)); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + var repro = BodyAs(result); + Assert.Equal(taskId, repro.TaskId); + } +}