From 968918773222a6c5f3c71f3122d4faf3083f5b44 Mon Sep 17 00:00:00 2001 From: Maxwell Weru Date: Tue, 19 Sep 2023 16:38:20 +0300 Subject: [PATCH] Use controllers in the server for easier testing (#807) * Reorganize models folder * Use controller for webhooks/azure * Use controllers for update_jobs/{id}/* * Use controllers for management endpoint --- .../Models/DependabotConfigurationTests.cs | 2 +- .../Models/DependabotUpdateScheduleTests.cs | 2 +- ... => WebhooksControllerIntegrationTests.cs} | 84 +++--- .../Workflow/UpdateRunnerTests.cs | 2 +- .../WorkflowBackgroundServiceTests.cs | 2 + server/Tingle.Dependabot/AuthConstants.cs | 13 + server/Tingle.Dependabot/AzureDevOpsEvent.cs | 240 ---------------- .../TriggerUpdateJobsEventConsumer.cs | 1 + .../Consumers/UpdateJobEventsConsumer.cs | 1 + .../Controllers/ManagementController.cs | 120 ++++++++ .../Controllers/UpdateJobsController.cs | 117 ++++++++ .../WebhooksController.cs} | 20 +- .../Events/RepositoryCreatedEvent.cs | 2 +- .../Models/Azure/AzureDevOpsEvent.cs | 24 ++ .../Azure/AzureDevOpsEventCodePushResource.cs | 21 ++ .../Azure/AzureDevOpsEventCommentResource.cs | 25 ++ ...OpsEventPullRequestCommentEventResource.cs | 15 + .../AzureDevOpsEventPullRequestResource.cs | 72 +++++ .../Models/Azure/AzureDevOpsEventRefUpdate.cs | 17 ++ .../Azure/AzureDevOpsEventRepository.cs | 38 +++ .../AzureDevOpsEventRepositoryProject.cs | 28 ++ .../Models/Azure/AzureDevOpsEventType.cs | 31 +++ .../Dependabot/DependabotChangedDependency.cs | 27 ++ .../DependabotClosePullRequestModel.cs | 17 ++ .../DependabotConfiguration.cs | 2 +- .../DependabotCreatePullRequestModel.cs | 23 ++ .../DependabotMarkAsProcessedModel.cs | 12 + .../DependabotRecordUpdateJobErrorModel.cs | 16 ++ .../DependabotUpdateDependencyListModel.cs | 20 ++ .../DependabotUpdatePullRequestModel.cs | 23 ++ .../DependabotUpdatedDependencyFile.cs | 33 +++ .../Models/DependabotUpdateJobDefinition.cs | 145 ---------- .../Tingle.Dependabot/Models/MainDbContext.cs | 1 + .../Models/{ => Management}/Repository.cs | 3 +- .../{ => Management}/RepositoryUpdate.cs | 3 +- .../SynchronizationRequest.cs | 2 +- .../{ => Management}/TriggerUpdateRequest.cs | 2 +- .../Models/{ => Management}/UpdateJob.cs | 2 +- .../{ => Management}/UpdateJobResources.cs | 2 +- .../{ => Management}/UpdateJobStatus.cs | 2 +- .../{ => Management}/UpdateJobTrigger.cs | 2 +- .../Models/PayloadWithData.cs | 12 + server/Tingle.Dependabot/Program.cs | 262 ++---------------- .../Workflow/AzureDevOpsProvider.cs | 2 +- .../Workflow/SchedulableUpdate.cs | 2 +- .../Workflow/Synchronizer.cs | 2 + .../Workflow/UpdateRunner.cs | 3 +- .../Workflow/UpdateScheduler.cs | 2 +- .../Workflow/WorkflowBackgroundService.cs | 2 + 49 files changed, 794 insertions(+), 707 deletions(-) rename server/Tingle.Dependabot.Tests/{AzureDevOpsEventHandlerTests.cs => WebhooksControllerIntegrationTests.cs} (78%) create mode 100644 server/Tingle.Dependabot/AuthConstants.cs delete mode 100644 server/Tingle.Dependabot/AzureDevOpsEvent.cs create mode 100644 server/Tingle.Dependabot/Controllers/ManagementController.cs create mode 100644 server/Tingle.Dependabot/Controllers/UpdateJobsController.cs rename server/Tingle.Dependabot/{AzureDevOpsEventHandler.cs => Controllers/WebhooksController.cs} (86%) create mode 100644 server/Tingle.Dependabot/Models/Azure/AzureDevOpsEvent.cs create mode 100644 server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventCodePushResource.cs create mode 100644 server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventCommentResource.cs create mode 100644 server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventPullRequestCommentEventResource.cs create mode 100644 server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventPullRequestResource.cs create mode 100644 server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventRefUpdate.cs create mode 100644 server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventRepository.cs create mode 100644 server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventRepositoryProject.cs create mode 100644 server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventType.cs create mode 100644 server/Tingle.Dependabot/Models/Dependabot/DependabotChangedDependency.cs create mode 100644 server/Tingle.Dependabot/Models/Dependabot/DependabotClosePullRequestModel.cs rename server/Tingle.Dependabot/Models/{ => Dependabot}/DependabotConfiguration.cs (99%) create mode 100644 server/Tingle.Dependabot/Models/Dependabot/DependabotCreatePullRequestModel.cs create mode 100644 server/Tingle.Dependabot/Models/Dependabot/DependabotMarkAsProcessedModel.cs create mode 100644 server/Tingle.Dependabot/Models/Dependabot/DependabotRecordUpdateJobErrorModel.cs create mode 100644 server/Tingle.Dependabot/Models/Dependabot/DependabotUpdateDependencyListModel.cs create mode 100644 server/Tingle.Dependabot/Models/Dependabot/DependabotUpdatePullRequestModel.cs create mode 100644 server/Tingle.Dependabot/Models/Dependabot/DependabotUpdatedDependencyFile.cs delete mode 100644 server/Tingle.Dependabot/Models/DependabotUpdateJobDefinition.cs rename server/Tingle.Dependabot/Models/{ => Management}/Repository.cs (95%) rename server/Tingle.Dependabot/Models/{ => Management}/RepositoryUpdate.cs (90%) rename server/Tingle.Dependabot/Models/{ => Management}/SynchronizationRequest.cs (85%) rename server/Tingle.Dependabot/Models/{ => Management}/TriggerUpdateRequest.cs (86%) rename server/Tingle.Dependabot/Models/{ => Management}/UpdateJob.cs (98%) rename server/Tingle.Dependabot/Models/{ => Management}/UpdateJobResources.cs (96%) rename server/Tingle.Dependabot/Models/{ => Management}/UpdateJobStatus.cs (67%) rename server/Tingle.Dependabot/Models/{ => Management}/UpdateJobTrigger.cs (86%) create mode 100644 server/Tingle.Dependabot/Models/PayloadWithData.cs diff --git a/server/Tingle.Dependabot.Tests/Models/DependabotConfigurationTests.cs b/server/Tingle.Dependabot.Tests/Models/DependabotConfigurationTests.cs index c9dc68ba..3b1b3caf 100644 --- a/server/Tingle.Dependabot.Tests/Models/DependabotConfigurationTests.cs +++ b/server/Tingle.Dependabot.Tests/Models/DependabotConfigurationTests.cs @@ -1,5 +1,5 @@ using System.ComponentModel.DataAnnotations; -using Tingle.Dependabot.Models; +using Tingle.Dependabot.Models.Dependabot; using Xunit; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; diff --git a/server/Tingle.Dependabot.Tests/Models/DependabotUpdateScheduleTests.cs b/server/Tingle.Dependabot.Tests/Models/DependabotUpdateScheduleTests.cs index a0d855f1..82141117 100644 --- a/server/Tingle.Dependabot.Tests/Models/DependabotUpdateScheduleTests.cs +++ b/server/Tingle.Dependabot.Tests/Models/DependabotUpdateScheduleTests.cs @@ -1,4 +1,4 @@ -using Tingle.Dependabot.Models; +using Tingle.Dependabot.Models.Dependabot; using Xunit; namespace Tingle.Dependabot.Tests.Models; diff --git a/server/Tingle.Dependabot.Tests/AzureDevOpsEventHandlerTests.cs b/server/Tingle.Dependabot.Tests/WebhooksControllerIntegrationTests.cs similarity index 78% rename from server/Tingle.Dependabot.Tests/AzureDevOpsEventHandlerTests.cs rename to server/Tingle.Dependabot.Tests/WebhooksControllerIntegrationTests.cs index 190595e6..4570f07f 100644 --- a/server/Tingle.Dependabot.Tests/AzureDevOpsEventHandlerTests.cs +++ b/server/Tingle.Dependabot.Tests/WebhooksControllerIntegrationTests.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging; using System.Net; using System.Text; +using System.Text.Json; using Tingle.Dependabot.Events; using Tingle.Dependabot.Models; using Tingle.EventBus; @@ -17,11 +18,11 @@ namespace Tingle.Dependabot.Tests; -public class AzureDevOpsEventHandlerTests +public class WebhooksControllerIntegrationTests { private readonly ITestOutputHelper outputHelper; - public AzureDevOpsEventHandlerTests(ITestOutputHelper outputHelper) + public WebhooksControllerIntegrationTests(ITestOutputHelper outputHelper) { this.outputHelper = outputHelper ?? throw new ArgumentNullException(nameof(outputHelper)); } @@ -29,7 +30,7 @@ public AzureDevOpsEventHandlerTests(ITestOutputHelper outputHelper) [Fact] public async Task Returns_Unauthorized() { - await TestAsync(async (harness, client, handler) => + await TestAsync(async (harness, client) => { // without Authorization header var request = new HttpRequestMessage(HttpMethod.Post, "/webhooks/azure"); @@ -44,7 +45,6 @@ await TestAsync(async (harness, client, handler) => response = await client.SendAsync(request); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); Assert.Empty(await response.Content.ReadAsStringAsync()); - Assert.Empty(handler.Calls); Assert.Empty(await harness.PublishedAsync()); }); } @@ -52,15 +52,18 @@ await TestAsync(async (harness, client, handler) => [Fact] public async Task Returns_BadRequest_NoBody() { - await TestAsync(async (harness, client, handler) => + await TestAsync(async (harness, client) => { var request = new HttpRequestMessage(HttpMethod.Post, "/webhooks/azure"); request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes("vsts:burp-bump"))); request.Content = new StringContent("", Encoding.UTF8, "application/json"); var response = await client.SendAsync(request); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - Assert.Empty(await response.Content.ReadAsStringAsync()); - Assert.Empty(handler.Calls); + var body = await response.Content.ReadAsStringAsync(); + Assert.Contains("\"type\":\"https://tools.ietf.org/html/rfc7231#section-6.5.1\"", body); + Assert.Contains("\"title\":\"One or more validation errors occurred.\"", body); + Assert.Contains("\"status\":400", body); + Assert.Contains("\"errors\":{\"\":[\"A non-empty request body is required.\"],\"model\":[\"The model field is required.\"]}", body); Assert.Empty(await harness.PublishedAsync()); }); } @@ -68,7 +71,7 @@ await TestAsync(async (harness, client, handler) => [Fact] public async Task Returns_BadRequest_MissingValues() { - await TestAsync(async (harness, client, handler) => + await TestAsync(async (harness, client) => { var request = new HttpRequestMessage(HttpMethod.Post, "/webhooks/azure"); request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes("vsts:burp-bump"))); @@ -82,7 +85,6 @@ await TestAsync(async (harness, client, handler) => Assert.Contains("\"SubscriptionId\":[\"The SubscriptionId field is required.\"]", body); Assert.Contains("\"EventType\":[\"The EventType field is required.\"]", body); Assert.Contains("\"Resource\":[\"The Resource field is required.\"]", body); - Assert.Empty(handler.Calls); Assert.Empty(await harness.PublishedAsync()); }); } @@ -90,7 +92,7 @@ await TestAsync(async (harness, client, handler) => [Fact] public async Task Returns_UnsupportedMediaType() { - await TestAsync(async (harness, client, handler) => + await TestAsync(async (harness, client) => { var stream = TestSamples.GetAzureDevOpsPullRequestUpdated1(); var request = new HttpRequestMessage(HttpMethod.Post, "/webhooks/azure"); @@ -98,8 +100,10 @@ await TestAsync(async (harness, client, handler) => request.Content = new StreamContent(stream); var response = await client.SendAsync(request); Assert.Equal(HttpStatusCode.UnsupportedMediaType, response.StatusCode); - Assert.Empty(await response.Content.ReadAsStringAsync()); - Assert.Empty(handler.Calls); + var body = await response.Content.ReadAsStringAsync(); + Assert.Contains("\"type\":\"https://tools.ietf.org/html/rfc7231#section-6.5.13\"", body); + Assert.Contains("\"title\":\"Unsupported Media Type\"", body); + Assert.Contains("\"status\":415", body); Assert.Empty(await harness.PublishedAsync()); }); } @@ -107,7 +111,7 @@ await TestAsync(async (harness, client, handler) => [Fact] public async Task Returns_OK_CodePush() { - await TestAsync(async (harness, client, handler) => + await TestAsync(async (harness, client) => { var stream = TestSamples.GetAzureDevOpsGitPush1(); var request = new HttpRequestMessage(HttpMethod.Post, "/webhooks/azure"); @@ -117,10 +121,6 @@ await TestAsync(async (harness, client, handler) => var response = await client.SendAsync(request); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Empty(await response.Content.ReadAsStringAsync()); - var call = Assert.Single(handler.Calls); - Assert.Equal("435e539d-3ce2-4283-8da9-8f3c0fe2e45e", call.SubscriptionId); - Assert.Equal(3, call.NotificationId); - Assert.Equal(AzureDevOpsEventType.GitPush, call.EventType); // Ensure the message was published var context = Assert.IsType>(Assert.Single(await harness.PublishedAsync(TimeSpan.FromSeconds(1f)))); @@ -135,7 +135,7 @@ await TestAsync(async (harness, client, handler) => [Fact] public async Task Returns_OK_PullRequestUpdated() { - await TestAsync(async (harness, client, handler) => + await TestAsync(async (harness, client) => { var stream = TestSamples.GetAzureDevOpsPullRequestUpdated1(); var request = new HttpRequestMessage(HttpMethod.Post, "/webhooks/azure"); @@ -145,10 +145,6 @@ await TestAsync(async (harness, client, handler) => var response = await client.SendAsync(request); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Empty(await response.Content.ReadAsStringAsync()); - var call = Assert.Single(handler.Calls); - Assert.Equal("435e539d-3ce2-4283-8da9-8f3c0fe2e45e", call.SubscriptionId); - Assert.Equal(3, call.NotificationId); - Assert.Equal(AzureDevOpsEventType.GitPullRequestUpdated, call.EventType); Assert.Empty(await harness.PublishedAsync()); }); } @@ -156,7 +152,7 @@ await TestAsync(async (harness, client, handler) => [Fact] public async Task Returns_OK_PullRequestMerged() { - await TestAsync(async (harness, client, handler) => + await TestAsync(async (harness, client) => { var stream = TestSamples.GetAzureDevOpsPullRequestMerged1(); var request = new HttpRequestMessage(HttpMethod.Post, "/webhooks/azure"); @@ -166,10 +162,6 @@ await TestAsync(async (harness, client, handler) => var response = await client.SendAsync(request); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Empty(await response.Content.ReadAsStringAsync()); - var call = Assert.Single(handler.Calls); - Assert.Equal("435e539d-3ce2-4283-8da9-8f3c0fe2e45e", call.SubscriptionId); - Assert.Equal(3, call.NotificationId); - Assert.Equal(AzureDevOpsEventType.GitPullRequestMerged, call.EventType); Assert.Empty(await harness.PublishedAsync()); }); } @@ -177,7 +169,7 @@ await TestAsync(async (harness, client, handler) => [Fact] public async Task Returns_OK_PullRequestCommentEvent() { - await TestAsync(async (harness, client, handler) => + await TestAsync(async (harness, client) => { var stream = TestSamples.GetAzureDevOpsPullRequestCommentEvent1(); var request = new HttpRequestMessage(HttpMethod.Post, "/webhooks/azure"); @@ -187,15 +179,11 @@ await TestAsync(async (harness, client, handler) => var response = await client.SendAsync(request); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Empty(await response.Content.ReadAsStringAsync()); - var call = Assert.Single(handler.Calls); - Assert.Equal("435e539d-3ce2-4283-8da9-8f3c0fe2e45e", call.SubscriptionId); - Assert.Equal(3, call.NotificationId); - Assert.Equal(AzureDevOpsEventType.GitPullRequestCommentEvent, call.EventType); Assert.Empty(await harness.PublishedAsync()); }); } - private async Task TestAsync(Func executeAndVerify) + private async Task TestAsync(Func executeAndVerify) { // Arrange var builder = new WebHostBuilder() @@ -209,6 +197,14 @@ private async Task TestAsync(Func { + services.AddControllers() + .AddApplicationPart(typeof(MainDbContext).Assembly) + .AddJsonOptions(options => + { + options.JsonSerializerOptions.AllowTrailingCommas = true; + options.JsonSerializerOptions.ReadCommentHandling = JsonCommentHandling.Skip; + }); + var dbName = Guid.NewGuid().ToString(); var configuration = context.Configuration; services.AddDbContext(options => @@ -217,8 +213,6 @@ private async Task TestAsync(Func(); services.AddAuthentication() .AddBasic(AuthConstants.SchemeNameServiceHooks, options => options.Realm = "Dependabot"); @@ -242,7 +236,7 @@ private async Task TestAsync(Func { - endpoints.MapWebhooks(); + endpoints.MapControllers(); }); }); using var server = new TestServer(builder); @@ -253,8 +247,6 @@ private async Task TestAsync(Func(); await context.Database.EnsureCreatedAsync(); - var handler = Assert.IsType(provider.GetRequiredService()); - var harness = provider.GetRequiredService(); await harness.StartAsync(); @@ -262,7 +254,7 @@ private async Task TestAsync(Func logger) - : base(publisher, logger) { } - - public List Calls { get; } = new(); - - public override async Task HandleAsync(AzureDevOpsEvent model, CancellationToken cancellationToken) - { - Calls.Add(model); - await base.HandleAsync(model, cancellationToken); - } - } } diff --git a/server/Tingle.Dependabot.Tests/Workflow/UpdateRunnerTests.cs b/server/Tingle.Dependabot.Tests/Workflow/UpdateRunnerTests.cs index bd654a54..63915c5d 100644 --- a/server/Tingle.Dependabot.Tests/Workflow/UpdateRunnerTests.cs +++ b/server/Tingle.Dependabot.Tests/Workflow/UpdateRunnerTests.cs @@ -1,4 +1,4 @@ -using Tingle.Dependabot.Models; +using Tingle.Dependabot.Models.Dependabot; using Tingle.Dependabot.Workflow; using Xunit; using Xunit.Abstractions; diff --git a/server/Tingle.Dependabot.Tests/Workflow/WorkflowBackgroundServiceTests.cs b/server/Tingle.Dependabot.Tests/Workflow/WorkflowBackgroundServiceTests.cs index 571e99ed..03ebb9e1 100644 --- a/server/Tingle.Dependabot.Tests/Workflow/WorkflowBackgroundServiceTests.cs +++ b/server/Tingle.Dependabot.Tests/Workflow/WorkflowBackgroundServiceTests.cs @@ -4,6 +4,8 @@ using Microsoft.Extensions.Logging; using Tingle.Dependabot.Events; using Tingle.Dependabot.Models; +using Tingle.Dependabot.Models.Dependabot; +using Tingle.Dependabot.Models.Management; using Tingle.Dependabot.Workflow; using Tingle.EventBus; using Tingle.EventBus.Transports.InMemory; diff --git a/server/Tingle.Dependabot/AuthConstants.cs b/server/Tingle.Dependabot/AuthConstants.cs new file mode 100644 index 00000000..449dae29 --- /dev/null +++ b/server/Tingle.Dependabot/AuthConstants.cs @@ -0,0 +1,13 @@ +namespace Tingle.Dependabot; + +internal static class AuthConstants +{ + // These values are fixed strings due to configuration sections + internal const string SchemeNameManagement = "Management"; + internal const string SchemeNameServiceHooks = "ServiceHooks"; + internal const string SchemeNameUpdater = "Updater"; + + internal const string PolicyNameManagement = "Management"; + internal const string PolicyNameServiceHooks = "ServiceHooks"; + internal const string PolicyNameUpdater = "Updater"; +} diff --git a/server/Tingle.Dependabot/AzureDevOpsEvent.cs b/server/Tingle.Dependabot/AzureDevOpsEvent.cs deleted file mode 100644 index 53217aa2..00000000 --- a/server/Tingle.Dependabot/AzureDevOpsEvent.cs +++ /dev/null @@ -1,240 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.Runtime.Serialization; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; - -namespace Tingle.Dependabot; - -public class AzureDevOpsEvent -{ - [Required] - [JsonPropertyName("subscriptionId")] - public string? SubscriptionId { get; set; } - - [Required] - [JsonPropertyName("notificationId")] - public int NotificationId { get; set; } - - [Required] - [JsonPropertyName("eventType")] - public AzureDevOpsEventType? EventType { get; set; } - - [Required] - [JsonPropertyName("resource")] - public JsonObject? Resource { get; set; } -} - -public class AzureDevOpsEventCodePushResource -{ - /// - /// List of updated references. - /// - [Required] - [JsonPropertyName("refUpdates")] - public List? RefUpdates { get; set; } - - /// - /// Details about the repository. - /// - [Required] - [JsonPropertyName("repository")] - public AzureDevOpsEventRepository? Repository { get; set; } -} - -public class AzureDevOpsEventPullRequestResource -{ - /// - /// Details about the repository. - /// - [Required] - [JsonPropertyName("repository")] - public AzureDevOpsEventRepository? Repository { get; set; } - - /// - /// The identifier of the Pull Request. - /// - [Required] - [JsonPropertyName("pullRequestId")] - public int PullRequestId { get; set; } - - /// - /// The status of the Pull Request. - /// - [Required] - [JsonPropertyName("status")] - public string? Status { get; set; } - - /// - /// The title of the Pull Request. - /// - [Required] - [JsonPropertyName("title")] - public string? Title { get; set; } - - /// - /// The branch of the repository from which the changes are picked from in the Pull Request. - /// - /// refs/heads/feature/my-feature - [Required] - [JsonPropertyName("sourceRefName")] - public string? SourceRefName { get; set; } - - /// - /// The branch of the repository to which the merge shall be done. - /// - /// refs/heads/main - [Required] - [JsonPropertyName("targetRefName")] - public string? TargetRefName { get; set; } - - /// - /// The status of the merge. - /// - [Required] - [JsonPropertyName("mergeStatus")] - public string? MergeStatus { get; set; } - - /// - /// The identifier of the merge. - /// - [Required] - [JsonPropertyName("mergeId")] - public string? MergeId { get; set; } - - /// - /// The URL for the Pull Request. - /// - [Required] - [JsonPropertyName("url")] - public string? Url { get; set; } -} - -public class AzureDevOpsEventPullRequestCommentEventResource -{ - [Required] - [JsonPropertyName("comment")] - public AzureDevOpsEventCommentResource? Comment { get; set; } - - [Required] - [JsonPropertyName("pullRequest")] - public AzureDevOpsEventPullRequestResource? PullRequest { get; set; } -} - -public class AzureDevOpsEventCommentResource -{ - [Required] - [JsonPropertyName("id")] - public int? Id { get; set; } - - [JsonPropertyName("parentCommentId")] - public int? ParentCommentId { get; set; } - - [Required] - [JsonPropertyName("content")] - public string? Content { get; set; } - - [JsonPropertyName("commentType")] - public string? CommentType { get; set; } - - [Required] - [JsonPropertyName("publishedDate")] - public DateTimeOffset? PublishedDate { get; set; } -} - -public class AzureDevOpsEventRefUpdate -{ - [Required] - [JsonPropertyName("name")] - public string? Name { get; set; } - - [JsonPropertyName("oldObjectId")] - public string? OldObjectId { get; set; } - - [JsonPropertyName("newObjectId")] - public string? NewObjectId { get; set; } -} - -public class AzureDevOpsEventRepository -{ - /// - /// The unique identifier of the repository. - /// - [Required] - [JsonPropertyName("id")] - public string? Id { get; set; } - - /// - /// The name of the repository. - /// - [Required] - [JsonPropertyName("name")] - public string? Name { get; set; } - - /// - /// The details about the project which owns the repository. - /// - [Required] - [JsonPropertyName("project")] - public AzureDevOpsEventRepositoryProject? Project { get; set; } - - /// - /// The default branch of the repository. - /// - [JsonPropertyName("defaultBranch")] - public string? DefaultBranch { get; set; } // should not be required because some repositories do not have default branches - - [Required] - [JsonPropertyName("remoteUrl")] - public string? RemoteUrl { get; set; } -} - -public class AzureDevOpsEventRepositoryProject -{ - /// - /// The unique identifier of the project. - /// - [Required] - [JsonPropertyName("id")] - public string? Id { get; set; } - - /// - /// The name of the project. - /// - [Required] - [JsonPropertyName("name")] - public string? Name { get; set; } - - /// - /// The URL for the project. - /// - [Required] - [JsonPropertyName("url")] - public string? Url { get; set; } -} - -[JsonConverter(typeof(JsonStringEnumMemberConverter))] -public enum AzureDevOpsEventType -{ - /// Code pushed - /// Code is pushed to a Git repository. - [EnumMember(Value = "git.push")] - GitPush, - - /// Pull request updated - /// - /// Pull request is updated – status, review list, reviewer vote - /// changed or the source branch is updated with a push. - /// - [EnumMember(Value = "git.pullrequest.updated")] - GitPullRequestUpdated, - - /// Pull request merge attempted - /// Pull request - Branch merge attempted. - [EnumMember(Value = "git.pullrequest.merged")] - GitPullRequestMerged, - - /// Pull request commented on - /// Comments are added to a pull request. - [EnumMember(Value = "ms.vss-code.git-pullrequest-comment-event")] - GitPullRequestCommentEvent, -} diff --git a/server/Tingle.Dependabot/Consumers/TriggerUpdateJobsEventConsumer.cs b/server/Tingle.Dependabot/Consumers/TriggerUpdateJobsEventConsumer.cs index 5a3cfa04..a2a923ae 100644 --- a/server/Tingle.Dependabot/Consumers/TriggerUpdateJobsEventConsumer.cs +++ b/server/Tingle.Dependabot/Consumers/TriggerUpdateJobsEventConsumer.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Tingle.Dependabot.Events; using Tingle.Dependabot.Models; +using Tingle.Dependabot.Models.Management; using Tingle.Dependabot.Workflow; using Tingle.EventBus; diff --git a/server/Tingle.Dependabot/Consumers/UpdateJobEventsConsumer.cs b/server/Tingle.Dependabot/Consumers/UpdateJobEventsConsumer.cs index 5aa86c7d..7cc0b93c 100644 --- a/server/Tingle.Dependabot/Consumers/UpdateJobEventsConsumer.cs +++ b/server/Tingle.Dependabot/Consumers/UpdateJobEventsConsumer.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Tingle.Dependabot.Events; using Tingle.Dependabot.Models; +using Tingle.Dependabot.Models.Management; using Tingle.Dependabot.Workflow; using Tingle.EventBus; diff --git a/server/Tingle.Dependabot/Controllers/ManagementController.cs b/server/Tingle.Dependabot/Controllers/ManagementController.cs new file mode 100644 index 00000000..513013a2 --- /dev/null +++ b/server/Tingle.Dependabot/Controllers/ManagementController.cs @@ -0,0 +1,120 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using Tingle.Dependabot.Events; +using Tingle.Dependabot.Models; +using Tingle.Dependabot.Models.Management; +using Tingle.Dependabot.Workflow; +using Tingle.EventBus; + +namespace Tingle.Dependabot.Controllers; + +[ApiController] +[Route("/mgnt")] +[Authorize(AuthConstants.PolicyNameManagement)] +public class ManagementController : ControllerBase // TODO: unit test this +{ + private readonly MainDbContext dbContext; + private readonly IEventPublisher publisher; + private readonly AzureDevOpsProvider adoProvider; + + public ManagementController(MainDbContext dbContext, IEventPublisher publisher, AzureDevOpsProvider adoProvider) + { + this.dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); + this.publisher = publisher ?? throw new ArgumentNullException(nameof(publisher)); + this.adoProvider = adoProvider ?? throw new ArgumentNullException(nameof(adoProvider)); + } + + [HttpPost("sync")] + public async Task SyncAsync([FromBody] SynchronizationRequest model) + { + // request synchronization of the project + var evt = new ProcessSynchronization(model.Trigger); + await publisher.PublishAsync(evt); + + return Ok(); + } + + [HttpPost("/webhooks/register")] + public async Task WebhooksRegisterAsync() + { + await adoProvider.CreateOrUpdateSubscriptionsAsync(); + return Ok(); + } + + [HttpGet("repos")] + public async Task GetReposAsync() + { + var repos = await dbContext.Repositories.ToListAsync(); + return Ok(repos); + } + + [HttpGet("repos/{id}")] + public async Task GetRepoAsync([FromRoute, Required] string id) + { + var repository = await dbContext.Repositories.SingleOrDefaultAsync(r => r.Id == id); + return Ok(repository); + } + + [HttpGet("repos/{id}/jobs/{jobId}")] + public async Task GetJobAsync([FromRoute, Required] string id, [FromRoute, Required] string jobId) + { + // ensure repository exists + var repository = await dbContext.Repositories.SingleOrDefaultAsync(r => r.Id == id); + if (repository is null) + { + return Problem(title: "repository_not_found", statusCode: 400); + } + + // find the job + var job = dbContext.UpdateJobs.Where(j => j.RepositoryId == repository.Id && j.Id == jobId).SingleOrDefaultAsync(); + return Ok(job); + } + + [HttpPost("repos/{id}/sync")] + public async Task SyncRepoAsync([FromRoute, Required] string id, [FromBody] SynchronizationRequest model) + { + // ensure repository exists + var repository = await dbContext.Repositories.SingleOrDefaultAsync(r => r.Id == id); + if (repository is null) + { + return Problem(title: "repository_not_found", statusCode: 400); + } + + // request synchronization of the repository + var evt = new ProcessSynchronization(model.Trigger, repositoryId: repository.Id, null); + await publisher.PublishAsync(evt); + + return Ok(repository); + } + + [HttpPost("repos/{id}/trigger")] + public async Task TriggerAsync([FromRoute, Required] string id, [FromBody] TriggerUpdateRequest model) + { + // ensure repository exists + var repository = await dbContext.Repositories.SingleOrDefaultAsync(r => r.Id == id); + if (repository is null) + { + return Problem(title: "repository_not_found", statusCode: 400); + } + + // ensure the repository update exists + var update = repository.Updates.ElementAtOrDefault(model.Id!.Value); + if (update is null) + { + return Problem(title: "repository_update_not_found", statusCode: 400); + } + + // trigger update for specific update + var evt = new TriggerUpdateJobsEvent + { + RepositoryId = repository.Id, + RepositoryUpdateId = model.Id.Value, + Trigger = UpdateJobTrigger.Manual, + }; + await publisher.PublishAsync(evt); + + return Ok(repository); + } +} diff --git a/server/Tingle.Dependabot/Controllers/UpdateJobsController.cs b/server/Tingle.Dependabot/Controllers/UpdateJobsController.cs new file mode 100644 index 00000000..60ef8e3a --- /dev/null +++ b/server/Tingle.Dependabot/Controllers/UpdateJobsController.cs @@ -0,0 +1,117 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using System.Text.Json; +using System.Text.Json.Nodes; +using Tingle.Dependabot.Events; +using Tingle.Dependabot.Models; +using Tingle.Dependabot.Models.Dependabot; +using Tingle.Dependabot.Models.Management; +using Tingle.EventBus; + +namespace Tingle.Dependabot.Controllers; + +[ApiController] +[Route("/update_jobs")] +[Authorize(AuthConstants.PolicyNameUpdater)] +public class UpdateJobsController : ControllerBase // TODO: unit and integration test this +{ + private readonly MainDbContext dbContext; + private readonly IEventPublisher publisher; + private readonly ILogger logger; + + public UpdateJobsController(MainDbContext dbContext, IEventPublisher publisher, ILogger logger) + { + this.dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); + this.publisher = publisher ?? throw new ArgumentNullException(nameof(publisher)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + // TODO: implement logic for *pull_request endpoints + + [HttpPost("{id}/create_pull_request")] + public async Task CreatePullRequestAsync([FromRoute, Required] string id, [FromBody] PayloadWithData model) + { + var job = await dbContext.UpdateJobs.SingleAsync(p => p.Id == id); + logger.LogInformation("Received request to create a pull request from job {JobId} but we did nothing.\r\n{ModelJson}", id, JsonSerializer.Serialize(model)); + return Ok(); + } + + [HttpPost("{id}/update_pull_request")] + public async Task UpdatePullRequestAsync([FromRoute, Required] string id, [FromBody] PayloadWithData model) + { + var job = await dbContext.UpdateJobs.SingleAsync(p => p.Id == id); + logger.LogInformation("Received request to update a pull request from job {JobId} but we did nothing.\r\n{ModelJson}", id, JsonSerializer.Serialize(model)); + return Ok(); + } + + [HttpPost("{id}/close_pull_request")] + public async Task ClosePullRequestAsync([FromRoute, Required] string id, [FromBody] PayloadWithData model) + { + var job = await dbContext.UpdateJobs.SingleAsync(p => p.Id == id); + logger.LogInformation("Received request to close a pull request from job {JobId} but we did nothing.\r\n{ModelJson}", id, JsonSerializer.Serialize(model)); + return Ok(); + } + + [HttpPost("{id}/record_update_job_error")] + public async Task RecordErrorAsync([FromRoute, Required] string id, [FromBody] PayloadWithData model) + { + var job = await dbContext.UpdateJobs.SingleAsync(p => p.Id == id); + + job.Error = new UpdateJobError + { + Type = model.Data!.ErrorType, + Detail = model.Data.ErrorDetail, + }; + + await dbContext.SaveChangesAsync(); + + return Ok(); + } + + [HttpPatch("{id}/mark_as_processed")] + public async Task MarkAsProcessedAsync([FromRoute, Required] string id, [FromBody] PayloadWithData model) + { + var job = await dbContext.UpdateJobs.SingleAsync(p => p.Id == id); + + // publish event that will run update the job and collect logs + var evt = new UpdateJobCheckStateEvent { JobId = id, }; + await publisher.PublishAsync(evt); + + return Ok(); + } + + [HttpPost("{id}/update_dependency_list")] + public async Task UpdateDependencyListAsync([FromRoute, Required] string id, [FromBody] PayloadWithData model) + { + var job = await dbContext.UpdateJobs.SingleAsync(p => p.Id == id); + var repository = await dbContext.Repositories.SingleAsync(r => r.Id == job.RepositoryId); + + // update the database + var update = repository.Updates.SingleOrDefault(u => u.PackageEcosystem == job.PackageEcosystem && u.Directory == job.Directory); + if (update is not null) + { + update.Files = model.Data?.DependencyFiles ?? new(); + } + await dbContext.SaveChangesAsync(); + + return Ok(); + } + + [HttpPost("{id}/record_ecosystem_versions")] + public async Task RecordEcosystemVersionsAsync([FromRoute, Required] string id, [FromBody] JsonNode model) + { + var job = await dbContext.UpdateJobs.SingleAsync(p => p.Id == id); + logger.LogInformation("Received request to record ecosystem version from job {JobId} but we did nothing.\r\n{ModelJson}", id, model.ToJsonString()); + return Ok(); + } + + [HttpPost("{id}/increment_metric")] + public async Task IncrementMetricAsync([FromRoute, Required] string id, [FromBody] JsonNode model) + { + var job = await dbContext.UpdateJobs.SingleAsync(p => p.Id == id); + logger.LogInformation("Received metrics from job {JobId} but we did nothing with them.\r\n{ModelJson}", id, model.ToJsonString()); + return Ok(); + } +} diff --git a/server/Tingle.Dependabot/AzureDevOpsEventHandler.cs b/server/Tingle.Dependabot/Controllers/WebhooksController.cs similarity index 86% rename from server/Tingle.Dependabot/AzureDevOpsEventHandler.cs rename to server/Tingle.Dependabot/Controllers/WebhooksController.cs index cb720d7a..667046a8 100644 --- a/server/Tingle.Dependabot/AzureDevOpsEventHandler.cs +++ b/server/Tingle.Dependabot/Controllers/WebhooksController.cs @@ -1,22 +1,28 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using System.Text.Json; using Tingle.Dependabot.Events; +using Tingle.Dependabot.Models.Azure; using Tingle.EventBus; -namespace Tingle.Dependabot; +namespace Tingle.Dependabot.Controllers; -internal class AzureDevOpsEventHandler +[ApiController] +[Route("/webhooks")] +[Authorize(AuthConstants.PolicyNameServiceHooks)] +public class WebhooksController : ControllerBase // TODO: unit test this { private readonly IEventPublisher publisher; private readonly ILogger logger; - public AzureDevOpsEventHandler(IEventPublisher publisher, ILogger logger) + public WebhooksController(IEventPublisher publisher, ILogger logger) { this.publisher = publisher ?? throw new ArgumentNullException(nameof(publisher)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public virtual async Task HandleAsync(AzureDevOpsEvent model, CancellationToken cancellationToken = default) + [HttpPost("azure")] + public async Task PostAsync([FromBody] AzureDevOpsEvent model) { var type = model.EventType; logger.LogDebug("Received {EventType} notification {NotificationId} on subscription {SubscriptionId}", @@ -37,7 +43,7 @@ public virtual async Task HandleAsync(AzureDevOpsEvent model, CancellationToken { // request synchronization of the repository var evt = new ProcessSynchronization(true, repositoryProviderId: adoRepositoryId); - await publisher.PublishAsync(evt, cancellationToken: cancellationToken); + await publisher.PublishAsync(evt); } } else if (type is AzureDevOpsEventType.GitPullRequestUpdated or AzureDevOpsEventType.GitPullRequestMerged) @@ -93,5 +99,7 @@ public virtual async Task HandleAsync(AzureDevOpsEvent model, CancellationToken { logger.LogWarning("'{EventType}' events are not supported!", type); } + + return Ok(); } } diff --git a/server/Tingle.Dependabot/Events/RepositoryCreatedEvent.cs b/server/Tingle.Dependabot/Events/RepositoryCreatedEvent.cs index e98350a7..f43fac45 100644 --- a/server/Tingle.Dependabot/Events/RepositoryCreatedEvent.cs +++ b/server/Tingle.Dependabot/Events/RepositoryCreatedEvent.cs @@ -1,4 +1,4 @@ -using Tingle.Dependabot.Models; +using Tingle.Dependabot.Models.Management; namespace Tingle.Dependabot.Events; diff --git a/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEvent.cs b/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEvent.cs new file mode 100644 index 00000000..4fee3b51 --- /dev/null +++ b/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEvent.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace Tingle.Dependabot.Models.Azure; + +public class AzureDevOpsEvent +{ + [Required] + [JsonPropertyName("subscriptionId")] + public string? SubscriptionId { get; set; } + + [Required] + [JsonPropertyName("notificationId")] + public int NotificationId { get; set; } + + [Required] + [JsonPropertyName("eventType")] + public AzureDevOpsEventType? EventType { get; set; } + + [Required] + [JsonPropertyName("resource")] + public JsonObject? Resource { get; set; } +} diff --git a/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventCodePushResource.cs b/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventCodePushResource.cs new file mode 100644 index 00000000..58077edb --- /dev/null +++ b/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventCodePushResource.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Tingle.Dependabot.Models.Azure; + +public class AzureDevOpsEventCodePushResource +{ + /// + /// List of updated references. + /// + [Required] + [JsonPropertyName("refUpdates")] + public List? RefUpdates { get; set; } + + /// + /// Details about the repository. + /// + [Required] + [JsonPropertyName("repository")] + public AzureDevOpsEventRepository? Repository { get; set; } +} diff --git a/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventCommentResource.cs b/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventCommentResource.cs new file mode 100644 index 00000000..9851a37a --- /dev/null +++ b/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventCommentResource.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Tingle.Dependabot.Models.Azure; + +public class AzureDevOpsEventCommentResource +{ + [Required] + [JsonPropertyName("id")] + public int? Id { get; set; } + + [JsonPropertyName("parentCommentId")] + public int? ParentCommentId { get; set; } + + [Required] + [JsonPropertyName("content")] + public string? Content { get; set; } + + [JsonPropertyName("commentType")] + public string? CommentType { get; set; } + + [Required] + [JsonPropertyName("publishedDate")] + public DateTimeOffset? PublishedDate { get; set; } +} diff --git a/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventPullRequestCommentEventResource.cs b/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventPullRequestCommentEventResource.cs new file mode 100644 index 00000000..6b06a5fe --- /dev/null +++ b/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventPullRequestCommentEventResource.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Tingle.Dependabot.Models.Azure; + +public class AzureDevOpsEventPullRequestCommentEventResource +{ + [Required] + [JsonPropertyName("comment")] + public AzureDevOpsEventCommentResource? Comment { get; set; } + + [Required] + [JsonPropertyName("pullRequest")] + public AzureDevOpsEventPullRequestResource? PullRequest { get; set; } +} diff --git a/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventPullRequestResource.cs b/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventPullRequestResource.cs new file mode 100644 index 00000000..b280ac63 --- /dev/null +++ b/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventPullRequestResource.cs @@ -0,0 +1,72 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Tingle.Dependabot.Models.Azure; + +public class AzureDevOpsEventPullRequestResource +{ + /// + /// Details about the repository. + /// + [Required] + [JsonPropertyName("repository")] + public AzureDevOpsEventRepository? Repository { get; set; } + + /// + /// The identifier of the Pull Request. + /// + [Required] + [JsonPropertyName("pullRequestId")] + public int PullRequestId { get; set; } + + /// + /// The status of the Pull Request. + /// + [Required] + [JsonPropertyName("status")] + public string? Status { get; set; } + + /// + /// The title of the Pull Request. + /// + [Required] + [JsonPropertyName("title")] + public string? Title { get; set; } + + /// + /// The branch of the repository from which the changes are picked from in the Pull Request. + /// + /// refs/heads/feature/my-feature + [Required] + [JsonPropertyName("sourceRefName")] + public string? SourceRefName { get; set; } + + /// + /// The branch of the repository to which the merge shall be done. + /// + /// refs/heads/main + [Required] + [JsonPropertyName("targetRefName")] + public string? TargetRefName { get; set; } + + /// + /// The status of the merge. + /// + [Required] + [JsonPropertyName("mergeStatus")] + public string? MergeStatus { get; set; } + + /// + /// The identifier of the merge. + /// + [Required] + [JsonPropertyName("mergeId")] + public string? MergeId { get; set; } + + /// + /// The URL for the Pull Request. + /// + [Required] + [JsonPropertyName("url")] + public string? Url { get; set; } +} diff --git a/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventRefUpdate.cs b/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventRefUpdate.cs new file mode 100644 index 00000000..a6ddadac --- /dev/null +++ b/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventRefUpdate.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Tingle.Dependabot.Models.Azure; + +public class AzureDevOpsEventRefUpdate +{ + [Required] + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("oldObjectId")] + public string? OldObjectId { get; set; } + + [JsonPropertyName("newObjectId")] + public string? NewObjectId { get; set; } +} diff --git a/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventRepository.cs b/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventRepository.cs new file mode 100644 index 00000000..7b456f8d --- /dev/null +++ b/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventRepository.cs @@ -0,0 +1,38 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Tingle.Dependabot.Models.Azure; + +public class AzureDevOpsEventRepository +{ + /// + /// The unique identifier of the repository. + /// + [Required] + [JsonPropertyName("id")] + public string? Id { get; set; } + + /// + /// The name of the repository. + /// + [Required] + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// + /// The details about the project which owns the repository. + /// + [Required] + [JsonPropertyName("project")] + public AzureDevOpsEventRepositoryProject? Project { get; set; } + + /// + /// The default branch of the repository. + /// + [JsonPropertyName("defaultBranch")] + public string? DefaultBranch { get; set; } // should not be required because some repositories do not have default branches + + [Required] + [JsonPropertyName("remoteUrl")] + public string? RemoteUrl { get; set; } +} diff --git a/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventRepositoryProject.cs b/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventRepositoryProject.cs new file mode 100644 index 00000000..54427c2e --- /dev/null +++ b/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventRepositoryProject.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Tingle.Dependabot.Models.Azure; + +public class AzureDevOpsEventRepositoryProject +{ + /// + /// The unique identifier of the project. + /// + [Required] + [JsonPropertyName("id")] + public string? Id { get; set; } + + /// + /// The name of the project. + /// + [Required] + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// + /// The URL for the project. + /// + [Required] + [JsonPropertyName("url")] + public string? Url { get; set; } +} diff --git a/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventType.cs b/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventType.cs new file mode 100644 index 00000000..faf79240 --- /dev/null +++ b/server/Tingle.Dependabot/Models/Azure/AzureDevOpsEventType.cs @@ -0,0 +1,31 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Tingle.Dependabot.Models.Azure; + +[JsonConverter(typeof(JsonStringEnumMemberConverter))] +public enum AzureDevOpsEventType +{ + /// Code pushed + /// Code is pushed to a Git repository. + [EnumMember(Value = "git.push")] + GitPush, + + /// Pull request updated + /// + /// Pull request is updated – status, review list, reviewer vote + /// changed or the source branch is updated with a push. + /// + [EnumMember(Value = "git.pullrequest.updated")] + GitPullRequestUpdated, + + /// Pull request merge attempted + /// Pull request - Branch merge attempted. + [EnumMember(Value = "git.pullrequest.merged")] + GitPullRequestMerged, + + /// Pull request commented on + /// Comments are added to a pull request. + [EnumMember(Value = "ms.vss-code.git-pullrequest-comment-event")] + GitPullRequestCommentEvent, +} diff --git a/server/Tingle.Dependabot/Models/Dependabot/DependabotChangedDependency.cs b/server/Tingle.Dependabot/Models/Dependabot/DependabotChangedDependency.cs new file mode 100644 index 00000000..6a6e66de --- /dev/null +++ b/server/Tingle.Dependabot/Models/Dependabot/DependabotChangedDependency.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace Tingle.Dependabot.Models.Dependabot; + +public class DependabotChangedDependency +{ + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("previous-version")] + public string? PreviousVersion { get; set; } + + [Required] + [JsonPropertyName("requirements")] + public JsonArray? Requirements { get; set; } + + [JsonPropertyName("previous-requirements")] + public string? PreviousRequirements { get; set; } + + [JsonPropertyName("version")] + public string? Version { get; set; } + + [JsonPropertyName("removed")] + public bool? Removed { get; set; } +} diff --git a/server/Tingle.Dependabot/Models/Dependabot/DependabotClosePullRequestModel.cs b/server/Tingle.Dependabot/Models/Dependabot/DependabotClosePullRequestModel.cs new file mode 100644 index 00000000..d52069a9 --- /dev/null +++ b/server/Tingle.Dependabot/Models/Dependabot/DependabotClosePullRequestModel.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; + +namespace Tingle.Dependabot.Models.Dependabot; + +public class DependabotClosePullRequestModel +{ + //[Required] + //[MinLength(1)] + //[JsonPropertyName("dependency-names")] + //public List? DependencyNames { get; set; } // This can also be a string that's why it has not been enabled + + [JsonPropertyName("reason")] + public string? Reason { get; set; } // convert from string to enum once we know all possible values + + [JsonExtensionData] + public Dictionary? Extensions { get; set; } +} diff --git a/server/Tingle.Dependabot/Models/DependabotConfiguration.cs b/server/Tingle.Dependabot/Models/Dependabot/DependabotConfiguration.cs similarity index 99% rename from server/Tingle.Dependabot/Models/DependabotConfiguration.cs rename to server/Tingle.Dependabot/Models/Dependabot/DependabotConfiguration.cs index 70de3e07..9db71fe1 100644 --- a/server/Tingle.Dependabot/Models/DependabotConfiguration.cs +++ b/server/Tingle.Dependabot/Models/Dependabot/DependabotConfiguration.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; -namespace Tingle.Dependabot.Models; +namespace Tingle.Dependabot.Models.Dependabot; public class DependabotConfiguration : IValidatableObject { diff --git a/server/Tingle.Dependabot/Models/Dependabot/DependabotCreatePullRequestModel.cs b/server/Tingle.Dependabot/Models/Dependabot/DependabotCreatePullRequestModel.cs new file mode 100644 index 00000000..ca9b86cb --- /dev/null +++ b/server/Tingle.Dependabot/Models/Dependabot/DependabotCreatePullRequestModel.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Tingle.Dependabot.Models.Dependabot; + +public class DependabotCreatePullRequestModel +{ + [Required] + [MinLength(1)] + [JsonPropertyName("dependencies")] + public List? Dependencies { get; set; } + + [Required] + [MinLength(1)] + [JsonPropertyName("updated-dependency-files")] + public List? DependencyFiles { get; set; } + + [JsonPropertyName("base-commit-sha")] + public string? BaseCommitSha { get; set; } + + [JsonExtensionData] + public Dictionary? Extensions { get; set; } +} diff --git a/server/Tingle.Dependabot/Models/Dependabot/DependabotMarkAsProcessedModel.cs b/server/Tingle.Dependabot/Models/Dependabot/DependabotMarkAsProcessedModel.cs new file mode 100644 index 00000000..d99620d1 --- /dev/null +++ b/server/Tingle.Dependabot/Models/Dependabot/DependabotMarkAsProcessedModel.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Tingle.Dependabot.Models.Dependabot; + +public class DependabotMarkAsProcessedModel +{ + [JsonPropertyName("base-commit-sha")] + public string? BaseCommitSha { get; set; } + + [JsonExtensionData] + public Dictionary? Extensions { get; set; } +} diff --git a/server/Tingle.Dependabot/Models/Dependabot/DependabotRecordUpdateJobErrorModel.cs b/server/Tingle.Dependabot/Models/Dependabot/DependabotRecordUpdateJobErrorModel.cs new file mode 100644 index 00000000..d2b72cb6 --- /dev/null +++ b/server/Tingle.Dependabot/Models/Dependabot/DependabotRecordUpdateJobErrorModel.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace Tingle.Dependabot.Models.Dependabot; + +public class DependabotRecordUpdateJobErrorModel +{ + [JsonPropertyName("error-type")] + public string? ErrorType { get; set; } + + [JsonPropertyName("error-detail")] + public JsonNode? ErrorDetail { get; set; } + + [JsonExtensionData] + public Dictionary? Extensions { get; set; } +} diff --git a/server/Tingle.Dependabot/Models/Dependabot/DependabotUpdateDependencyListModel.cs b/server/Tingle.Dependabot/Models/Dependabot/DependabotUpdateDependencyListModel.cs new file mode 100644 index 00000000..0cf37d86 --- /dev/null +++ b/server/Tingle.Dependabot/Models/Dependabot/DependabotUpdateDependencyListModel.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace Tingle.Dependabot.Models.Dependabot; + +public class DependabotUpdateDependencyListModel +{ + [Required] + [JsonPropertyName("dependencies")] + public JsonArray? Dependencies { get; set; } + + [Required] + [MinLength(1)] + [JsonPropertyName("dependency_files")] + public List? DependencyFiles { get; set; } + + [JsonExtensionData] + public Dictionary? Extensions { get; set; } +} diff --git a/server/Tingle.Dependabot/Models/Dependabot/DependabotUpdatePullRequestModel.cs b/server/Tingle.Dependabot/Models/Dependabot/DependabotUpdatePullRequestModel.cs new file mode 100644 index 00000000..6d4f3660 --- /dev/null +++ b/server/Tingle.Dependabot/Models/Dependabot/DependabotUpdatePullRequestModel.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Tingle.Dependabot.Models.Dependabot; + +public class DependabotUpdatePullRequestModel +{ + [Required] + [MinLength(1)] + [JsonPropertyName("dependency-names")] + public List? DependencyNames { get; set; } + + [Required] + [MinLength(1)] + [JsonPropertyName("updated-dependency-files")] + public List? DependencyFiles { get; set; } + + [JsonPropertyName("base-commit-sha")] + public string? BaseCommitSha { get; set; } + + [JsonExtensionData] + public Dictionary? Extensions { get; set; } +} diff --git a/server/Tingle.Dependabot/Models/Dependabot/DependabotUpdatedDependencyFile.cs b/server/Tingle.Dependabot/Models/Dependabot/DependabotUpdatedDependencyFile.cs new file mode 100644 index 00000000..99a8c00e --- /dev/null +++ b/server/Tingle.Dependabot/Models/Dependabot/DependabotUpdatedDependencyFile.cs @@ -0,0 +1,33 @@ +using System.Text.Json.Serialization; + +namespace Tingle.Dependabot.Models.Dependabot; + +public class DependabotUpdatedDependencyFile +{ + [JsonPropertyName("content")] + public string? Content { get; set; } + + [JsonPropertyName("content_encoding")] + public string? ContentEncoding { get; set; } + + [JsonPropertyName("deleted")] + public bool? Deleted { get; set; } + + [JsonPropertyName("directory")] + public string? Directory { get; set; } + + [JsonPropertyName("mode")] + public string? Mode { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("operation")] + public string? Operation { get; set; } // convert from string to enum once we know all possible values + + [JsonPropertyName("support_file")] + public bool? SupportFile { get; set; } + + [JsonPropertyName("type")] + public string? Type { get; set; } // convert from string to enum once we know all possible values +} diff --git a/server/Tingle.Dependabot/Models/DependabotUpdateJobDefinition.cs b/server/Tingle.Dependabot/Models/DependabotUpdateJobDefinition.cs deleted file mode 100644 index 7374ec23..00000000 --- a/server/Tingle.Dependabot/Models/DependabotUpdateJobDefinition.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; - -namespace Tingle.Dependabot.Models; - -public sealed class CreatePullRequestModel -{ - [Required] - [MinLength(1)] - [JsonPropertyName("dependencies")] - public List? Dependencies { get; set; } - - [Required] - [MinLength(1)] - [JsonPropertyName("updated-dependency-files")] - public List? DependencyFiles { get; set; } - - [JsonPropertyName("base-commit-sha")] - public string? BaseCommitSha { get; set; } - - [JsonExtensionData] - public Dictionary? Extensions { get; set; } -} - -public sealed class ChangedDependency -{ - [JsonPropertyName("name")] - public string? Name { get; set; } - - [JsonPropertyName("previous-version")] - public string? PreviousVersion { get; set; } - - [Required] - [JsonPropertyName("requirements")] - public JsonArray? Requirements { get; set; } - - [JsonPropertyName("previous-requirements")] - public string? PreviousRequirements { get; set; } - - [JsonPropertyName("version")] - public string? Version { get; set; } - - [JsonPropertyName("removed")] - public bool? Removed { get; set; } -} - -public sealed class UpdatePullRequestModel -{ - [Required] - [MinLength(1)] - [JsonPropertyName("dependency-names")] - public List? DependencyNames { get; set; } - - [Required] - [MinLength(1)] - [JsonPropertyName("updated-dependency-files")] - public List? DependencyFiles { get; set; } - - [JsonPropertyName("base-commit-sha")] - public string? BaseCommitSha { get; set; } - - [JsonExtensionData] - public Dictionary? Extensions { get; set; } -} - -public sealed class UpdatedDependencyFile -{ - [JsonPropertyName("content")] - public string? Content { get; set; } - - [JsonPropertyName("content_encoding")] - public string? ContentEncoding { get; set; } - - [JsonPropertyName("deleted")] - public bool? Deleted { get; set; } - - [JsonPropertyName("directory")] - public string? Directory { get; set; } - - [JsonPropertyName("mode")] - public string? Mode { get; set; } - - [JsonPropertyName("name")] - public string? Name { get; set; } - - [JsonPropertyName("operation")] - public string? Operation { get; set; } // convert from string to enum once we know all possible values - - [JsonPropertyName("support_file")] - public bool? SupportFile { get; set; } - - [JsonPropertyName("type")] - public string? Type { get; set; } // convert from string to enum once we know all possible values -} - -public sealed class ClosePullRequestModel -{ - //[Required] - //[MinLength(1)] - //[JsonPropertyName("dependency-names")] - //public List? DependencyNames { get; set; } // This can also be a string that's why it has not been enabled - - [JsonPropertyName("reason")] - public string? Reason { get; set; } // convert from string to enum once we know all possible values - - [JsonExtensionData] - public Dictionary? Extensions { get; set; } -} - -public sealed class RecordUpdateJobErrorModel -{ - [JsonPropertyName("error-type")] - public string? ErrorType { get; set; } - - [JsonPropertyName("error-detail")] - public JsonNode? ErrorDetail { get; set; } - - [JsonExtensionData] - public Dictionary? Extensions { get; set; } -} - -public sealed class MarkAsProcessedModel -{ - [JsonPropertyName("base-commit-sha")] - public string? BaseCommitSha { get; set; } - - [JsonExtensionData] - public Dictionary? Extensions { get; set; } -} - -public sealed class UpdateDependencyListModel -{ - [Required] - [JsonPropertyName("dependencies")] - public JsonArray? Dependencies { get; set; } - - [Required] - [MinLength(1)] - [JsonPropertyName("dependency_files")] - public List? DependencyFiles { get; set; } - - [JsonExtensionData] - public Dictionary? Extensions { get; set; } -} diff --git a/server/Tingle.Dependabot/Models/MainDbContext.cs b/server/Tingle.Dependabot/Models/MainDbContext.cs index ea8f6a9a..f5c0b1e9 100644 --- a/server/Tingle.Dependabot/Models/MainDbContext.cs +++ b/server/Tingle.Dependabot/Models/MainDbContext.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using System.Text.Json; +using Tingle.Dependabot.Models.Management; namespace Tingle.Dependabot.Models; diff --git a/server/Tingle.Dependabot/Models/Repository.cs b/server/Tingle.Dependabot/Models/Management/Repository.cs similarity index 95% rename from server/Tingle.Dependabot/Models/Repository.cs rename to server/Tingle.Dependabot/Models/Management/Repository.cs index 678af7c6..d2bc4b69 100644 --- a/server/Tingle.Dependabot/Models/Repository.cs +++ b/server/Tingle.Dependabot/Models/Management/Repository.cs @@ -1,7 +1,8 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; +using Tingle.Dependabot.Models.Dependabot; -namespace Tingle.Dependabot.Models; +namespace Tingle.Dependabot.Models.Management; public class Repository { diff --git a/server/Tingle.Dependabot/Models/RepositoryUpdate.cs b/server/Tingle.Dependabot/Models/Management/RepositoryUpdate.cs similarity index 90% rename from server/Tingle.Dependabot/Models/RepositoryUpdate.cs rename to server/Tingle.Dependabot/Models/Management/RepositoryUpdate.cs index 6beaa09d..b8b4b77c 100644 --- a/server/Tingle.Dependabot/Models/RepositoryUpdate.cs +++ b/server/Tingle.Dependabot/Models/Management/RepositoryUpdate.cs @@ -1,6 +1,7 @@ using System.Text.Json.Serialization; +using Tingle.Dependabot.Models.Dependabot; -namespace Tingle.Dependabot.Models; +namespace Tingle.Dependabot.Models.Management; public record RepositoryUpdate : DependabotUpdate { diff --git a/server/Tingle.Dependabot/Models/SynchronizationRequest.cs b/server/Tingle.Dependabot/Models/Management/SynchronizationRequest.cs similarity index 85% rename from server/Tingle.Dependabot/Models/SynchronizationRequest.cs rename to server/Tingle.Dependabot/Models/Management/SynchronizationRequest.cs index e9883b6a..66b14f6e 100644 --- a/server/Tingle.Dependabot/Models/SynchronizationRequest.cs +++ b/server/Tingle.Dependabot/Models/Management/SynchronizationRequest.cs @@ -1,4 +1,4 @@ -namespace Tingle.Dependabot.Models; +namespace Tingle.Dependabot.Models.Management; /// /// Represents a model for processing a synchronization request diff --git a/server/Tingle.Dependabot/Models/TriggerUpdateRequest.cs b/server/Tingle.Dependabot/Models/Management/TriggerUpdateRequest.cs similarity index 86% rename from server/Tingle.Dependabot/Models/TriggerUpdateRequest.cs rename to server/Tingle.Dependabot/Models/Management/TriggerUpdateRequest.cs index 3faa5932..05b3f13e 100644 --- a/server/Tingle.Dependabot/Models/TriggerUpdateRequest.cs +++ b/server/Tingle.Dependabot/Models/Management/TriggerUpdateRequest.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Tingle.Dependabot.Models; +namespace Tingle.Dependabot.Models.Management; /// /// Represents a model for triggering an update job. diff --git a/server/Tingle.Dependabot/Models/UpdateJob.cs b/server/Tingle.Dependabot/Models/Management/UpdateJob.cs similarity index 98% rename from server/Tingle.Dependabot/Models/UpdateJob.cs rename to server/Tingle.Dependabot/Models/Management/UpdateJob.cs index f01c3eef..ad99fdf3 100644 --- a/server/Tingle.Dependabot/Models/UpdateJob.cs +++ b/server/Tingle.Dependabot/Models/Management/UpdateJob.cs @@ -2,7 +2,7 @@ using System.Text.Json.Nodes; using System.Text.Json.Serialization; -namespace Tingle.Dependabot.Models; +namespace Tingle.Dependabot.Models.Management; // This class independent of one-to-many relationships for detached and prolonged tracking. // The records are cleaned up on a schedule. diff --git a/server/Tingle.Dependabot/Models/UpdateJobResources.cs b/server/Tingle.Dependabot/Models/Management/UpdateJobResources.cs similarity index 96% rename from server/Tingle.Dependabot/Models/UpdateJobResources.cs rename to server/Tingle.Dependabot/Models/Management/UpdateJobResources.cs index be076246..0a651a78 100644 --- a/server/Tingle.Dependabot/Models/UpdateJobResources.cs +++ b/server/Tingle.Dependabot/Models/Management/UpdateJobResources.cs @@ -1,7 +1,7 @@ using Azure.ResourceManager.AppContainers.Models; using System.ComponentModel.DataAnnotations; -namespace Tingle.Dependabot.Models; +namespace Tingle.Dependabot.Models.Management; public class UpdateJobResources { diff --git a/server/Tingle.Dependabot/Models/UpdateJobStatus.cs b/server/Tingle.Dependabot/Models/Management/UpdateJobStatus.cs similarity index 67% rename from server/Tingle.Dependabot/Models/UpdateJobStatus.cs rename to server/Tingle.Dependabot/Models/Management/UpdateJobStatus.cs index 17ab6c3c..11643af6 100644 --- a/server/Tingle.Dependabot/Models/UpdateJobStatus.cs +++ b/server/Tingle.Dependabot/Models/Management/UpdateJobStatus.cs @@ -1,4 +1,4 @@ -namespace Tingle.Dependabot.Models; +namespace Tingle.Dependabot.Models.Management; public enum UpdateJobStatus { diff --git a/server/Tingle.Dependabot/Models/UpdateJobTrigger.cs b/server/Tingle.Dependabot/Models/Management/UpdateJobTrigger.cs similarity index 86% rename from server/Tingle.Dependabot/Models/UpdateJobTrigger.cs rename to server/Tingle.Dependabot/Models/Management/UpdateJobTrigger.cs index dc54b1fe..75ff8fa4 100644 --- a/server/Tingle.Dependabot/Models/UpdateJobTrigger.cs +++ b/server/Tingle.Dependabot/Models/Management/UpdateJobTrigger.cs @@ -1,7 +1,7 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -namespace Tingle.Dependabot.Models; +namespace Tingle.Dependabot.Models.Management; [JsonConverter(typeof(JsonStringEnumMemberConverter))] public enum UpdateJobTrigger diff --git a/server/Tingle.Dependabot/Models/PayloadWithData.cs b/server/Tingle.Dependabot/Models/PayloadWithData.cs new file mode 100644 index 00000000..962148bd --- /dev/null +++ b/server/Tingle.Dependabot/Models/PayloadWithData.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace Tingle.Dependabot.Models; + +public class PayloadWithData where T : new() +{ + [Required] + public T? Data { get; set; } + + [System.Text.Json.Serialization.JsonExtensionData] + public Dictionary? Extensions { get; set; } +} diff --git a/server/Tingle.Dependabot/Program.cs b/server/Tingle.Dependabot/Program.cs index bb4e9d63..faf0243a 100644 --- a/server/Tingle.Dependabot/Program.cs +++ b/server/Tingle.Dependabot/Program.cs @@ -2,18 +2,12 @@ using AspNetCore.Authentication.Basic; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Diagnostics.HealthChecks; -using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using MiniValidation; -using System.ComponentModel.DataAnnotations; using System.Text.Json; -using System.Text.Json.Nodes; using Tingle.Dependabot; using Tingle.Dependabot.Consumers; -using Tingle.Dependabot.Events; using Tingle.Dependabot.Models; using Tingle.Dependabot.Workflow; -using Tingle.EventBus; var builder = WebApplication.CreateBuilder(args); @@ -33,6 +27,14 @@ // Add data protection builder.Services.AddDataProtection().PersistKeysToDbContext(); +// Add controllers +builder.Services.AddControllers() + .AddControllersAsServices() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.AllowTrailingCommas = true; + options.JsonSerializerOptions.ReadCommentHandling = JsonCommentHandling.Skip; + }); // Configure any generated URL to be in lower case builder.Services.Configure(options => options.LowercaseUrls = true); @@ -67,13 +69,13 @@ builder.Services.AddDistributedMemoryCache(); // Configure other services -builder.Services.ConfigureHttpJsonOptions(options => -{ - options.SerializerOptions.AllowTrailingCommas = true; - options.SerializerOptions.ReadCommentHandling = JsonCommentHandling.Skip; -}); -builder.Services.AddNotificationsHandler(); -builder.Services.AddWorkflowServices(builder.Configuration.GetSection("Workflow")); +builder.Services.Configure(builder.Configuration.GetSection("Workflow")); +builder.Services.ConfigureOptions(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddHostedService(); // Add event bus var selectedTransport = builder.Configuration.GetValue("EventBus:SelectedTransport"); @@ -111,9 +113,7 @@ app.MapHealthChecks("/health"); app.MapHealthChecks("/liveness", new HealthCheckOptions { Predicate = _ => false, }); -app.MapWebhooks(); -app.MapManagementApi(); -app.MapUpdateJobsApi(); +app.MapControllers(); // setup the application environment await AppSetup.SetupAsync(app); @@ -121,233 +121,3 @@ await app.RunAsync(); internal enum EventBusTransportKind { InMemory, ServiceBus, } - -internal static class AuthConstants -{ - // These values are fixed strings due to configuration sections - internal const string SchemeNameManagement = "Management"; - internal const string SchemeNameServiceHooks = "ServiceHooks"; - internal const string SchemeNameUpdater = "Updater"; - - internal const string PolicyNameManagement = "Management"; - internal const string PolicyNameServiceHooks = "ServiceHooks"; - internal const string PolicyNameUpdater = "Updater"; -} - -internal static class ApplicationExtensions -{ - public static IServiceCollection AddNotificationsHandler(this IServiceCollection services) - { - services.AddScoped(); - return services; - } - - public static IServiceCollection AddWorkflowServices(this IServiceCollection services, IConfiguration configuration) - { - services.Configure(configuration); - services.ConfigureOptions(); - - services.AddSingleton(); - services.AddSingleton(); - - services.AddScoped(); - services.AddScoped(); - services.AddHostedService(); - - return services; - } - - public static IEndpointConventionBuilder MapWebhooks(this IEndpointRouteBuilder builder) - { - var endpoint = builder.MapPost("/webhooks/azure", async (AzureDevOpsEventHandler handler, [FromBody] AzureDevOpsEvent model) => - { - if (!MiniValidator.TryValidate(model, out var errors)) return Results.ValidationProblem(errors); - - await handler.HandleAsync(model); - return Results.Ok(); - }); - - endpoint.RequireAuthorization(AuthConstants.PolicyNameServiceHooks); - - return endpoint; - } - - public static IEndpointRouteBuilder MapManagementApi(this IEndpointRouteBuilder builder) - { - var group = builder.MapGroup(""); - group.RequireAuthorization(AuthConstants.PolicyNameManagement); - - group.MapPost("/sync", async (IEventPublisher publisher, [FromBody] SynchronizationRequest model) => - { - // request synchronization of the project - var evt = new ProcessSynchronization(model.Trigger); - await publisher.PublishAsync(evt); - - return Results.Ok(); - }); - - group.MapPost("/webhooks/register/azure", async (AzureDevOpsProvider adoProvider) => - { - await adoProvider.CreateOrUpdateSubscriptionsAsync(); - return Results.Ok(); - }); - - group.MapGet("repos", async (MainDbContext dbContext) => Results.Ok(await dbContext.Repositories.ToListAsync())); - group.MapGet("repos/{id}", async (MainDbContext dbContext, [FromRoute, Required] string id) => Results.Ok(await dbContext.Repositories.SingleOrDefaultAsync(r => r.Id == id))); - group.MapPost("repos/{id}/sync", async (IEventPublisher publisher, MainDbContext dbContext, [FromRoute, Required] string id, [FromBody] SynchronizationRequest model) => - { - if (!MiniValidator.TryValidate(model, out var errors)) return Results.ValidationProblem(errors); - - // ensure repository exists - var repository = await dbContext.Repositories.SingleOrDefaultAsync(r => r.Id == id); - if (repository is null) - { - return Results.Problem(title: "repository_not_found", statusCode: 400); - } - - // request synchronization of the repository - var evt = new ProcessSynchronization(model.Trigger, repositoryId: repository.Id, null); - await publisher.PublishAsync(evt); - - return Results.Ok(repository); - }); - group.MapGet("repos/{id}/jobs/{jobId}", async (MainDbContext dbContext, [FromRoute, Required] string id, [FromRoute, Required] string jobId) => - { - // ensure repository exists - var repository = await dbContext.Repositories.SingleOrDefaultAsync(r => r.Id == id); - if (repository is null) - { - return Results.Problem(title: "repository_not_found", statusCode: 400); - } - - // find the job - var job = dbContext.UpdateJobs.Where(j => j.RepositoryId == repository.Id && j.Id == jobId).SingleOrDefaultAsync(); - return Results.Ok(job); - }); - group.MapPost("repos/{id}/trigger", async (IEventPublisher publisher, MainDbContext dbContext, [FromRoute, Required] string id, [FromBody] TriggerUpdateRequest model) => - { - if (!MiniValidator.TryValidate(model, out var errors)) return Results.ValidationProblem(errors); - - // ensure repository exists - var repository = await dbContext.Repositories.SingleOrDefaultAsync(r => r.Id == id); - if (repository is null) - { - return Results.Problem(title: "repository_not_found", statusCode: 400); - } - - // ensure the repository update exists - var update = repository.Updates.ElementAtOrDefault(model.Id!.Value); - if (update is null) - { - return Results.Problem(title: "repository_update_not_found", statusCode: 400); - } - - // trigger update for specific update - var evt = new TriggerUpdateJobsEvent - { - RepositoryId = repository.Id, - RepositoryUpdateId = model.Id.Value, - Trigger = UpdateJobTrigger.Manual, - }; - await publisher.PublishAsync(evt); - - return Results.Ok(repository); - }); - - return builder; - } - - public static IEndpointRouteBuilder MapUpdateJobsApi(this IEndpointRouteBuilder builder) - { - var logger = builder.ServiceProvider.GetRequiredService().CreateLogger("UpdateJobsApi"); - - // endpoints accessed by the updater during execution - - var group = builder.MapGroup("update_jobs"); - group.RequireAuthorization(AuthConstants.PolicyNameUpdater); - - // TODO: implement logic for *pull_request endpoints - group.MapPost("/{id}/create_pull_request", async (MainDbContext dbContext, [FromRoute, Required] string id, [FromBody] PayloadWithData model) => - { - var job = await dbContext.UpdateJobs.SingleAsync(p => p.Id == id); - logger.LogInformation("Received request to create a pull request from job {JobId} but we did nothing.\r\n{ModelJson}", id, JsonSerializer.Serialize(model)); - return Results.Ok(); - }); - group.MapPost("/{id}/update_pull_request", async (MainDbContext dbContext, [FromRoute, Required] string id, [FromBody] PayloadWithData model) => - { - var job = await dbContext.UpdateJobs.SingleAsync(p => p.Id == id); - logger.LogInformation("Received request to update a pull request from job {JobId} but we did nothing.\r\n{ModelJson}", id, JsonSerializer.Serialize(model)); - return Results.Ok(); - }); - group.MapPost("/{id}/close_pull_request", async (MainDbContext dbContext, [FromRoute, Required] string id, [FromBody] PayloadWithData model) => - { - var job = await dbContext.UpdateJobs.SingleAsync(p => p.Id == id); - logger.LogInformation("Received request to close a pull request from job {JobId} but we did nothing.\r\n{ModelJson}", id, JsonSerializer.Serialize(model)); - return Results.Ok(); - }); - - group.MapPost("/{id}/record_update_job_error", async (MainDbContext dbContext, [FromRoute, Required] string id, [FromBody] PayloadWithData model) => - { - var job = await dbContext.UpdateJobs.SingleAsync(p => p.Id == id); - - job.Error = new UpdateJobError - { - Type = model.Data!.ErrorType, - Detail = model.Data.ErrorDetail, - }; - - await dbContext.SaveChangesAsync(); - - return Results.Ok(); - }); - group.MapPatch("/{id}/mark_as_processed", async (IEventPublisher publisher, MainDbContext dbContext, [FromRoute, Required] string id, [FromBody] PayloadWithData model) => - { - var job = await dbContext.UpdateJobs.SingleAsync(p => p.Id == id); - - // publish event that will run update the job and collect logs - var evt = new UpdateJobCheckStateEvent { JobId = id, }; - await publisher.PublishAsync(evt); - - return Results.Ok(); - }); - group.MapPost("/{id}/update_dependency_list", async (MainDbContext dbContext, [FromRoute, Required] string id, [FromBody] PayloadWithData model) => - { - var job = await dbContext.UpdateJobs.SingleAsync(p => p.Id == id); - var repository = await dbContext.Repositories.SingleAsync(r => r.Id == job.RepositoryId); - - // update the database - var update = repository.Updates.SingleOrDefault(u => u.PackageEcosystem == job.PackageEcosystem && u.Directory == job.Directory); - if (update is not null) - { - update.Files = model.Data?.DependencyFiles ?? new(); - } - await dbContext.SaveChangesAsync(); - - return Results.Ok(); - }); - - group.MapPost("/{id}/record_ecosystem_versions", async (MainDbContext dbContext, [FromRoute, Required] string id, [FromBody] JsonNode model) => - { - var job = await dbContext.UpdateJobs.SingleAsync(p => p.Id == id); - logger.LogInformation("Received request to record ecosystem version from job {JobId} but we did nothing.\r\n{ModelJson}", id, model.ToJsonString()); - return Results.Ok(); - }); - group.MapPost("/{id}/increment_metric", async (MainDbContext dbContext, [FromRoute, Required] string id, [FromBody] JsonNode model) => - { - var job = await dbContext.UpdateJobs.SingleAsync(p => p.Id == id); - logger.LogInformation("Received metrics from job {JobId} but we did nothing with them.\r\n{ModelJson}", id, model.ToJsonString()); - return Results.Ok(); - }); - - return builder; - } - - public class PayloadWithData where T : new() - { - [Required] - public T? Data { get; set; } - - [System.Text.Json.Serialization.JsonExtensionData] - public Dictionary? Extensions { get; set; } - } -} diff --git a/server/Tingle.Dependabot/Workflow/AzureDevOpsProvider.cs b/server/Tingle.Dependabot/Workflow/AzureDevOpsProvider.cs index d9367fac..2bd55263 100644 --- a/server/Tingle.Dependabot/Workflow/AzureDevOpsProvider.cs +++ b/server/Tingle.Dependabot/Workflow/AzureDevOpsProvider.cs @@ -11,7 +11,7 @@ namespace Tingle.Dependabot.Workflow; -internal class AzureDevOpsProvider +public class AzureDevOpsProvider { private static readonly (string, string)[] SubscriptionEventTypes = { diff --git a/server/Tingle.Dependabot/Workflow/SchedulableUpdate.cs b/server/Tingle.Dependabot/Workflow/SchedulableUpdate.cs index a826521e..415e9e98 100644 --- a/server/Tingle.Dependabot/Workflow/SchedulableUpdate.cs +++ b/server/Tingle.Dependabot/Workflow/SchedulableUpdate.cs @@ -1,4 +1,4 @@ -using Tingle.Dependabot.Models; +using Tingle.Dependabot.Models.Dependabot; namespace Tingle.Dependabot.Workflow; diff --git a/server/Tingle.Dependabot/Workflow/Synchronizer.cs b/server/Tingle.Dependabot/Workflow/Synchronizer.cs index bc34f5ad..ab9fe78f 100644 --- a/server/Tingle.Dependabot/Workflow/Synchronizer.cs +++ b/server/Tingle.Dependabot/Workflow/Synchronizer.cs @@ -3,6 +3,8 @@ using System.ComponentModel.DataAnnotations; using Tingle.Dependabot.Events; using Tingle.Dependabot.Models; +using Tingle.Dependabot.Models.Dependabot; +using Tingle.Dependabot.Models.Management; using Tingle.EventBus; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; diff --git a/server/Tingle.Dependabot/Workflow/UpdateRunner.cs b/server/Tingle.Dependabot/Workflow/UpdateRunner.cs index c4c642ba..72ac7dce 100644 --- a/server/Tingle.Dependabot/Workflow/UpdateRunner.cs +++ b/server/Tingle.Dependabot/Workflow/UpdateRunner.cs @@ -9,7 +9,8 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Text.RegularExpressions; -using Tingle.Dependabot.Models; +using Tingle.Dependabot.Models.Dependabot; +using Tingle.Dependabot.Models.Management; namespace Tingle.Dependabot.Workflow; diff --git a/server/Tingle.Dependabot/Workflow/UpdateScheduler.cs b/server/Tingle.Dependabot/Workflow/UpdateScheduler.cs index c57bba4e..984d70b9 100644 --- a/server/Tingle.Dependabot/Workflow/UpdateScheduler.cs +++ b/server/Tingle.Dependabot/Workflow/UpdateScheduler.cs @@ -1,6 +1,6 @@ using System.Collections.Concurrent; using Tingle.Dependabot.Events; -using Tingle.Dependabot.Models; +using Tingle.Dependabot.Models.Management; using Tingle.EventBus; using Tingle.PeriodicTasks; diff --git a/server/Tingle.Dependabot/Workflow/WorkflowBackgroundService.cs b/server/Tingle.Dependabot/Workflow/WorkflowBackgroundService.cs index ddce53a7..36c5bd2c 100644 --- a/server/Tingle.Dependabot/Workflow/WorkflowBackgroundService.cs +++ b/server/Tingle.Dependabot/Workflow/WorkflowBackgroundService.cs @@ -1,6 +1,8 @@ using Microsoft.EntityFrameworkCore; using Tingle.Dependabot.Events; using Tingle.Dependabot.Models; +using Tingle.Dependabot.Models.Dependabot; +using Tingle.Dependabot.Models.Management; using Tingle.EventBus; using Tingle.PeriodicTasks;