From 73156d244d384198d615bfa2be9f2ae7ac414f34 Mon Sep 17 00:00:00 2001 From: David Bennett Date: Mon, 5 Feb 2024 14:41:37 -0800 Subject: [PATCH] Add Windows toast notifications for new reviews for the logged in developer's pull requests. (#337) * Add review to datastore and notifications --- .../DataManager/GitHubDataManager.cs | 59 ++++- .../DataManager/GitHubDataManagerUpdate.cs | 6 + .../DataModel/DataObjects/Notification.cs | 99 +++++++- .../DataModel/DataObjects/PullRequest.cs | 20 ++ .../DataModel/DataObjects/Review.cs | 213 ++++++++++++++++++ .../DataModel/Enums/NotificationType.cs | 1 + .../DataModel/GitHubDataStoreSchema.cs | 17 +- .../Strings/en-US/Resources.resw | 8 + .../DataStore/DataObjectTests.cs | 69 +++++- .../DataStore/OctokitIngestionTests.cs | 49 ++++ 10 files changed, 532 insertions(+), 9 deletions(-) create mode 100644 src/GitHubExtension/DataModel/DataObjects/Review.cs diff --git a/src/GitHubExtension/DataManager/GitHubDataManager.cs b/src/GitHubExtension/DataManager/GitHubDataManager.cs index 5d4cfee8..2b3031ec 100644 --- a/src/GitHubExtension/DataManager/GitHubDataManager.cs +++ b/src/GitHubExtension/DataManager/GitHubDataManager.cs @@ -16,6 +16,7 @@ public partial class GitHubDataManager : IGitHubDataManager, IDisposable private static readonly TimeSpan NotificationRetentionTime = TimeSpan.FromDays(7); private static readonly TimeSpan SearchRetentionTime = TimeSpan.FromDays(7); private static readonly TimeSpan PullRequestStaleTime = TimeSpan.FromDays(1); + private static readonly TimeSpan ReviewStaleTime = TimeSpan.FromDays(7); // It is possible different widgets have queries which touch the same pull requests. // We want to keep this window large enough that we don't delete data being used by @@ -438,6 +439,27 @@ private async Task UpdatePullRequestsForLoggedInDeveloperIdsAsync(DataStoreOpera CommitCombinedStatus.GetOrCreate(DataStore, commitCombinedStatus); CreatePullRequestStatus(dsPullRequest); + + // Review information for this pull request. + // We will only get review data for the logged-in Developer's pull requests. + try + { + var octoReviews = await devId.GitHubClient.PullRequest.Review.GetAll(repoName[0], repoName[1], octoPull.Number); + foreach (var octoReview in octoReviews) + { + ProcessReview(dsPullRequest, octoReview); + } + } + catch (Exception e) + { + // Octokit can sometimes fail unexpectedly or have bugs. Should that occur here, we + // will not stop processing all pull requests and instead skip over getting the PR + // review information for this particular pull request. + Log.Logger()?.ReportError($"Error updating Reviews for Pull Request #{octoPull.Number}: {e.Message}"); + + // Put the full stack trace in debug if this occurs to reduce log spam. + Log.Logger()?.ReportDebug($"Error updating Reviews for Pull Request #{octoPull.Number}.", e); + } } Log.Logger()?.ReportDebug(Name, $"Updated developer pull requests for {repoFullName}."); @@ -514,6 +536,36 @@ private async Task UpdatePullRequestsAsync(Repository repository, Octokit.GitHub PullRequest.DeleteLastObservedBefore(DataStore, repository.Id, DateTime.UtcNow - LastObservedDeleteSpan); } + private void ProcessReview(PullRequest pullRequest, Octokit.PullRequestReview octoReview) + { + // Skip reviews that are stale. + if ((DateTime.Now - octoReview.SubmittedAt) > ReviewStaleTime) + { + return; + } + + // For creating review notifications, must first determine if the review has changed. + var existingReview = Review.GetByInternalId(DataStore, octoReview.Id); + + // Add/update the review record. + var newReview = Review.GetOrCreateByOctokitReview(DataStore, octoReview, pullRequest.Id); + + // Ignore comments or pending state. + if (string.IsNullOrEmpty(newReview.State) || newReview.State == "Commented") + { + Log.Logger()?.ReportDebug(Name, "Notifications", $"Ignoring review for {pullRequest}. State: {newReview.State}"); + return; + } + + // Create a new notification if the state is different or the review did not exist. + if (existingReview == null || (existingReview.State != newReview.State)) + { + // We assume that the logged in developer created this pull request. + Log.Logger()?.ReportInfo(Name, "Notifications", $"Creating NewReview Notification for {pullRequest}. State: {newReview.State}"); + Notification.Create(DataStore, newReview, NotificationType.NewReview); + } + } + private void CreatePullRequestStatus(PullRequest pullRequest) { // Get the previous status for comparison. @@ -525,15 +577,13 @@ private void CreatePullRequestStatus(PullRequest pullRequest) if (ShouldCreateCheckFailureNotification(curStatus, prevStatus)) { Log.Logger()?.ReportInfo(Name, "Notifications", $"Creating CheckRunFailure Notification for {curStatus}"); - var notification = Notification.Create(curStatus, NotificationType.CheckRunFailed); - Notification.Add(DataStore, notification); + Notification.Create(DataStore, curStatus, NotificationType.CheckRunFailed); } if (ShouldCreateCheckSucceededNotification(curStatus, prevStatus)) { Log.Logger()?.ReportDebug(Name, "Notifications", $"Creating CheckRunSuccess Notification for {curStatus}"); - var notification = Notification.Create(curStatus, NotificationType.CheckRunSucceeded); - Notification.Add(DataStore, notification); + Notification.Create(DataStore, curStatus, NotificationType.CheckRunSucceeded); } } @@ -662,6 +712,7 @@ private void PruneObsoleteData() Notification.DeleteBefore(DataStore, DateTime.Now - NotificationRetentionTime); Search.DeleteBefore(DataStore, DateTime.Now - SearchRetentionTime); SearchIssue.DeleteUnreferenced(DataStore); + Review.DeleteUnreferenced(DataStore); } // Sets a last-updated in the MetaData. diff --git a/src/GitHubExtension/DataManager/GitHubDataManagerUpdate.cs b/src/GitHubExtension/DataManager/GitHubDataManagerUpdate.cs index 4c1a91f8..9db4fc50 100644 --- a/src/GitHubExtension/DataManager/GitHubDataManagerUpdate.cs +++ b/src/GitHubExtension/DataManager/GitHubDataManagerUpdate.cs @@ -47,6 +47,12 @@ private static async Task UpdateDeveloperPullRequests() { notification.ShowToast(); } + + // Show notifications for new reviews. + if (notification.Type == NotificationType.NewReview) + { + notification.ShowToast(); + } } } diff --git a/src/GitHubExtension/DataModel/DataObjects/Notification.cs b/src/GitHubExtension/DataModel/DataObjects/Notification.cs index 950c0f27..14b3fa01 100644 --- a/src/GitHubExtension/DataModel/DataObjects/Notification.cs +++ b/src/GitHubExtension/DataModel/DataObjects/Notification.cs @@ -161,6 +161,7 @@ public bool ShowToast() { NotificationType.CheckRunFailed => ShowFailedCheckRunToast(), NotificationType.CheckRunSucceeded => ShowSucceededCheckRunToast(), + NotificationType.NewReview => ShowNewReviewToast(), _ => false, }; } @@ -225,9 +226,52 @@ private bool ShowSucceededCheckRunToast() return true; } - public static Notification Create(PullRequestStatus status, NotificationType type) + private bool ShowNewReviewToast() { - return new Notification + try + { + Notifications.Log.Logger()?.ReportInfo($"Showing Notification for {this}"); + var resLoader = new ResourceLoader(ResourceLoader.GetDefaultResourceFilePath(), "GitHubExtension/Resources"); + var nb = new AppNotificationBuilder(); + nb.SetDuration(AppNotificationDuration.Long); + nb.AddArgument("htmlurl", HtmlUrl); + + switch (Result) + { + case "Approved": + nb.AddText($"✅ {resLoader.GetString("Notifications_Toast_NewReview/Approved")}"); + break; + + case "ChangesRequested": + nb.AddText($"⚠️ {resLoader.GetString("Notifications_Toast_NewReview/ChangesRequested")}"); + break; + + default: + throw new ArgumentException($"Unknown Review Result: {Result}"); + } + + nb.AddText($"#{Identifier} - {Repository.FullName}", new AppNotificationTextProperties().SetMaxLines(1)); + + // We want to show Author login but the AppNotification has a max 3 AddText calls, see: + // https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.windows.appnotifications.builder.appnotificationbuilder.addtext?view=windows-app-sdk-1.2 + // The newline is a workaround to the 3 line restriction to show the Author line. + nb.AddText(Title + Environment.NewLine + "@" + User.Login); + nb.AddButton(new AppNotificationButton(resLoader.GetString("Notifications_Toast_Button/Dismiss")).AddArgument("action", "dismiss")); + AppNotificationManager.Default.Show(nb.BuildNotification()); + Toasted = true; + } + catch (Exception ex) + { + Notifications.Log.Logger()?.ReportError($"Failed creating the Notification for {this}", ex); + return false; + } + + return true; + } + + public static Notification Create(DataStore dataStore, PullRequestStatus status, NotificationType type) + { + var pullRequestNotification = new Notification { TypeId = (long)type, UserId = status.PullRequest.AuthorId, @@ -242,6 +286,33 @@ public static Notification Create(PullRequestStatus status, NotificationType typ TimeOccurred = status.TimeOccurred, TimeCreated = DateTime.Now.ToDataStoreInteger(), }; + + Add(dataStore, pullRequestNotification); + SetOlderNotificationsToasted(dataStore, pullRequestNotification); + return pullRequestNotification; + } + + public static Notification Create(DataStore dataStore, Review review, NotificationType type) + { + var reviewNotification = new Notification + { + TypeId = (long)type, + UserId = review.AuthorId, + RepositoryId = review.PullRequest.RepositoryId, + Title = review.PullRequest.Title, + Description = review.Body, + Identifier = review.PullRequest.Number.ToStringInvariant(), + Result = review.State, + HtmlUrl = review.HtmlUrl, + DetailsUrl = review.HtmlUrl, + ToastState = 0, + TimeOccurred = review.TimeSubmitted, + TimeCreated = DateTime.Now.ToDataStoreInteger(), + }; + + Add(dataStore, reviewNotification); + SetOlderNotificationsToasted(dataStore, reviewNotification); + return reviewNotification; } public static Notification Add(DataStore dataStore, Notification notification) @@ -272,6 +343,30 @@ public static IEnumerable Get(DataStore dataStore, DateTime? since return notifications; } + public static void SetOlderNotificationsToasted(DataStore dataStore, Notification notification) + { + // Get all untoasted notifications for the same type, identifier, and author that are older + // than the specified notification. + var sql = @"SELECT * FROM Notification WHERE TypeId = @TypeId AND RepositoryId = @RepositoryId AND Identifier = @Identifier AND UserId = @UserId AND TimeOccurred < @TimeOccurred AND ToastState = 0"; + var param = new + { + notification.TypeId, + notification.RepositoryId, + notification.Identifier, + notification.UserId, + notification.TimeOccurred, + }; + + Log.Logger()?.ReportDebug(DataStore.GetSqlLogMessage(sql, param)); + var outDatedNotifications = dataStore.Connection!.Query(sql, param, null) ?? Enumerable.Empty(); + foreach (var olderNotification in outDatedNotifications) + { + olderNotification.DataStore = dataStore; + olderNotification.Toasted = true; + Notifications.Log.Logger()?.ReportInfo($"Found older notification for {olderNotification.Identifier} with result {olderNotification.Result}, marking toasted."); + } + } + public static void DeleteBefore(DataStore dataStore, DateTime date) { // Delete notifications older than the date listed. diff --git a/src/GitHubExtension/DataModel/DataObjects/PullRequest.cs b/src/GitHubExtension/DataModel/DataObjects/PullRequest.cs index 4ac02178..f7e02e42 100644 --- a/src/GitHubExtension/DataModel/DataObjects/PullRequest.cs +++ b/src/GitHubExtension/DataModel/DataObjects/PullRequest.cs @@ -317,6 +317,26 @@ public PullRequestStatus? PullRequestStatus } } + /// + /// Gets all reviews associated with this pull request. + /// + [Write(false)] + [Computed] + public IEnumerable Reviews + { + get + { + if (DataStore == null) + { + return Enumerable.Empty(); + } + else + { + return Review.GetAllForPullRequest(DataStore, this) ?? Enumerable.Empty(); + } + } + } + public override string ToString() => $"{Number}: {Title}"; // Create pull request from OctoKit pull request data diff --git a/src/GitHubExtension/DataModel/DataObjects/Review.cs b/src/GitHubExtension/DataModel/DataObjects/Review.cs new file mode 100644 index 00000000..7fc76860 --- /dev/null +++ b/src/GitHubExtension/DataModel/DataObjects/Review.cs @@ -0,0 +1,213 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using Dapper; +using Dapper.Contrib.Extensions; +using GitHubExtension.Helpers; + +namespace GitHubExtension.DataModel; + +[Table("Review")] +public class Review +{ + [Key] + public long Id { get; set; } = DataStore.NoForeignKey; + + public long InternalId { get; set; } = DataStore.NoForeignKey; + + // Pull request table + public long PullRequestId { get; set; } = DataStore.NoForeignKey; + + // User table + public long AuthorId { get; set; } = DataStore.NoForeignKey; + + public string Body { get; set; } = string.Empty; + + public string State { get; set; } = string.Empty; + + public string HtmlUrl { get; set; } = string.Empty; + + public long TimeSubmitted { get; set; } = DataStore.NoForeignKey; + + public long TimeLastObserved { get; set; } = DataStore.NoForeignKey; + + [Write(false)] + private DataStore? DataStore + { + get; set; + } + + [Write(false)] + [Computed] + public DateTime SubmittedAt => TimeSubmitted.ToDateTime(); + + [Write(false)] + [Computed] + public DateTime LastObservedAt => TimeLastObserved.ToDateTime(); + + [Write(false)] + [Computed] + public PullRequest PullRequest + { + get + { + if (DataStore == null) + { + return new PullRequest(); + } + else + { + return PullRequest.GetById(DataStore, PullRequestId) ?? new PullRequest(); + } + } + } + + [Write(false)] + [Computed] + public User Author + { + get + { + if (DataStore == null) + { + return new User(); + } + else + { + return User.GetById(DataStore, AuthorId) ?? new User(); + } + } + } + + public override string ToString() => $"{PullRequestId}: {AuthorId} - {State}"; + + // Create review from OctoKit review data + private static Review CreateFromOctokitReview(DataStore dataStore, Octokit.PullRequestReview okitReview, long pullRequestId) + { + var review = new Review + { + DataStore = dataStore, + InternalId = okitReview.Id, + Body = okitReview.Body ?? string.Empty, + State = okitReview.State.Value.ToString(), + HtmlUrl = okitReview.HtmlUrl ?? string.Empty, + TimeSubmitted = okitReview.SubmittedAt.DateTime.ToDataStoreInteger(), + TimeLastObserved = DateTime.UtcNow.ToDataStoreInteger(), + }; + + // Author is a rowid in the User table + var author = User.GetOrCreateByOctokitUser(dataStore, okitReview.User); + review.AuthorId = author.Id; + + // Repo is a row id in the Repository table. + // It is likely the case that we already know the repository id (such as when querying pulls for a repository). + if (pullRequestId != DataStore.NoForeignKey) + { + review.PullRequestId = pullRequestId; + } + + return review; + } + + private static Review AddOrUpdateReview(DataStore dataStore, Review review) + { + // Check for existing pull request data. + var existingReview = GetByInternalId(dataStore, review.InternalId); + if (existingReview is not null) + { + // Existing pull requests must always be updated to update the LastObserved time. + review.Id = existingReview.Id; + dataStore.Connection!.Update(review); + review.DataStore = dataStore; + return review; + } + + // No existing pull request, add it. + review.Id = dataStore.Connection!.Insert(review); + review.DataStore = dataStore; + return review; + } + + public static Review? GetById(DataStore dataStore, long id) + { + var review = dataStore.Connection!.Get(id); + if (review is not null) + { + // Add Datastore so this object can make internal queries. + review.DataStore = dataStore; + } + + return review; + } + + public static Review? GetByInternalId(DataStore dataStore, long internalId) + { + var sql = @"SELECT * FROM Review WHERE InternalId = @InternalId;"; + var param = new + { + InternalId = internalId, + }; + + var review = dataStore.Connection!.QueryFirstOrDefault(sql, param, null); + if (review is not null) + { + // Add Datastore so this object can make internal queries. + review.DataStore = dataStore; + } + + return review; + } + + public static Review GetOrCreateByOctokitReview(DataStore dataStore, Octokit.PullRequestReview octokitReview, long repositoryId = DataStore.NoForeignKey) + { + var newReview = CreateFromOctokitReview(dataStore, octokitReview, repositoryId); + return AddOrUpdateReview(dataStore, newReview); + } + + public static IEnumerable GetAllForPullRequest(DataStore dataStore, PullRequest pullRequest) + { + var sql = @"SELECT * FROM Review WHERE PullRequestId = @PullRequestId ORDER BY TimeSubmitted DESC;"; + var param = new + { + PullRequestId = pullRequest.Id, + }; + + Log.Logger()?.ReportDebug(DataStore.GetSqlLogMessage(sql, param)); + var reviews = dataStore.Connection!.Query(sql, param, null) ?? Enumerable.Empty(); + foreach (var review in reviews) + { + review.DataStore = dataStore; + } + + return reviews; + } + + public static IEnumerable GetAllForUser(DataStore dataStore, User user) + { + var sql = @"SELECT * FROM Review WHERE AuthorId = @AuthorId;"; + var param = new + { + AuthorId = user.Id, + }; + + Log.Logger()?.ReportDebug(DataStore.GetSqlLogMessage(sql, param)); + var reviews = dataStore.Connection!.Query(sql, param, null) ?? Enumerable.Empty(); + foreach (var review in reviews) + { + review.DataStore = dataStore; + } + + return reviews; + } + + public static void DeleteUnreferenced(DataStore dataStore) + { + // Delete any reviews that have no matching PullRequestId in the PullRequest table. + var sql = @"DELETE FROM Review WHERE PullRequestId NOT IN (SELECT Id FROM PullRequest)"; + var command = dataStore.Connection!.CreateCommand(); + command.CommandText = sql; + Log.Logger()?.ReportDebug(DataStore.GetCommandLogMessage(sql, command)); + var rowsDeleted = command.ExecuteNonQuery(); + Log.Logger()?.ReportDebug(DataStore.GetDeletedLogMessage(rowsDeleted)); + } +} diff --git a/src/GitHubExtension/DataModel/Enums/NotificationType.cs b/src/GitHubExtension/DataModel/Enums/NotificationType.cs index 9a4f54e0..2091ae09 100644 --- a/src/GitHubExtension/DataModel/Enums/NotificationType.cs +++ b/src/GitHubExtension/DataModel/Enums/NotificationType.cs @@ -8,4 +8,5 @@ public enum NotificationType Unknown = 0, CheckRunFailed = 1, CheckRunSucceeded = 2, + NewReview = 3, } diff --git a/src/GitHubExtension/DataModel/GitHubDataStoreSchema.cs b/src/GitHubExtension/DataModel/GitHubDataStoreSchema.cs index 9d586bde..fd84a953 100644 --- a/src/GitHubExtension/DataModel/GitHubDataStoreSchema.cs +++ b/src/GitHubExtension/DataModel/GitHubDataStoreSchema.cs @@ -13,7 +13,7 @@ public GitHubDataStoreSchema() } // Update this anytime incompatible changes happen with a released version. - private const long SchemaVersionValue = 0x0005; + private const long SchemaVersionValue = 0x0006; private static readonly string Metadata = @"CREATE TABLE Metadata (" + @@ -233,6 +233,20 @@ public GitHubDataStoreSchema() ");" + "CREATE UNIQUE INDEX IDX_SearchIssue_SearchIssue ON SearchIssue (Search, Issue);"; + private static readonly string Review = + @"CREATE TABLE Review (" + + "Id INTEGER PRIMARY KEY NOT NULL," + + "InternalId INTEGER NOT NULL," + + "PullRequestId INTEGER NOT NULL," + + "AuthorId INTEGER NOT NULL," + + "Body TEXT NOT NULL COLLATE NOCASE," + + "State TEXT NOT NULL COLLATE NOCASE," + + "HtmlUrl TEXT NULL COLLATE NOCASE," + + "TimeSubmitted INTEGER NOT NULL," + + "TimeLastObserved INTEGER NOT NULL" + + ");" + + "CREATE UNIQUE INDEX IDX_Review_InternalId ON Review (InternalId);"; + // All Sqls together. private static readonly List SchemaSqlsValue = new () { @@ -253,5 +267,6 @@ public GitHubDataStoreSchema() Notification, Search, SearchIssue, + Review, }; } diff --git a/src/GitHubExtension/Strings/en-US/Resources.resw b/src/GitHubExtension/Strings/en-US/Resources.resw index 9f74e894..57b89cc4 100644 --- a/src/GitHubExtension/Strings/en-US/Resources.resw +++ b/src/GitHubExtension/Strings/en-US/Resources.resw @@ -501,4 +501,12 @@ Please enter the PAT LoginUI Error text if input is null + + Approved + Shown in toast notification. + + + Changes requested + Shown in toast notification. + \ No newline at end of file diff --git a/test/GitHubExtension/DataStore/DataObjectTests.cs b/test/GitHubExtension/DataStore/DataObjectTests.cs index 90504ce8..0de7acf2 100644 --- a/test/GitHubExtension/DataStore/DataObjectTests.cs +++ b/test/GitHubExtension/DataStore/DataObjectTests.cs @@ -515,8 +515,7 @@ public void ReadAndWriteStatusAndNotification() Assert.AreEqual(1, prStatus.ConclusionId); // Create notification from PR Status - var notification = DataModel.Notification.Create(prStatus, NotificationType.CheckRunFailed); - notification = DataModel.Notification.Add(dataStore, notification); + var notification = Notification.Create(dataStore, prStatus, NotificationType.CheckRunFailed); Assert.IsNotNull(notification); Assert.AreEqual("Fix the things", notification.Title); Assert.AreEqual(1, notification.RepositoryId); @@ -529,4 +528,70 @@ public void ReadAndWriteStatusAndNotification() testListener.PrintEventCounts(); Assert.AreEqual(false, testListener.FoundErrors()); } + + [TestMethod] + [TestCategory("Unit")] + public void ReadAndWriteReview() + { + using var log = new Logger("TestStore", TestOptions.LogOptions); + var testListener = new TestListener("TestListener", TestContext!); + log.AddListener(testListener); + Log.Attach(log); + + using var dataStore = new DataStore("TestStore", TestHelpers.GetDataStoreFilePath(TestOptions), TestOptions.DataStoreOptions.DataStoreSchema!); + Assert.IsNotNull(dataStore); + dataStore.Create(); + Assert.IsNotNull(dataStore.Connection); + + using var tx = dataStore.Connection!.BeginTransaction(); + + var reviews = new List + { + { new Review { PullRequestId = 1, AuthorId = 2, Body = "Review 1", InternalId = 16, State = "Approved" } }, + { new Review { PullRequestId = 1, AuthorId = 3, Body = "Review 2", InternalId = 47, State = "Rejected" } }, + }; + + dataStore.Connection.Insert(reviews[0]); + dataStore.Connection.Insert(reviews[1]); + + // Add User record + dataStore.Connection.Insert(new User { Login = "Kittens", InternalId = 6, AvatarUrl = "https://www.microsoft.com", Type = "Cat" }); + dataStore.Connection.Insert(new User { Login = "Doggos", InternalId = 83, AvatarUrl = "https://www.microsoft.com", Type = "Dog" }); + dataStore.Connection.Insert(new User { Login = "Lizards", InternalId = 3, AvatarUrl = "https://www.microsoft.com", Type = "Reptile" }); + + // Add repository record + dataStore.Connection.Insert(new Repository { OwnerId = 1, InternalId = 47, Name = "TestRepo1", Description = "Short Desc", HtmlUrl = "https://www.microsoft.com", DefaultBranch = "main", HasIssues = 1 }); + + // Add PullRequest record + dataStore.Connection.Insert(new PullRequest { Title = "Fix the things", InternalId = 42, HeadSha = "1234abcd", AuthorId = 1, RepositoryId = 1 }); + + tx.Commit(); + + // Verify retrieval and input into data objects. + var dataStoreReviews = dataStore.Connection.GetAll().ToList(); + Assert.AreEqual(2, dataStoreReviews.Count); + foreach (var review in dataStoreReviews) + { + TestContext?.WriteLine($" Review: {review.Id}: {review.Body} - {review.State}"); + + Assert.IsTrue(review.Id == 1 || review.Id == 2); + Assert.AreEqual(review.Body, $"Review {review.Id}"); + Assert.IsTrue(review.AuthorId == review.Id + 1); + } + + // Verify objects work. + var pullrequest = PullRequest.GetById(dataStore, 1); + Assert.IsNotNull(pullrequest); + var reviewsForPullRequest = pullrequest.Reviews; + Assert.IsNotNull(reviewsForPullRequest); + Assert.AreEqual(2, reviewsForPullRequest.Count()); + foreach (var review in reviewsForPullRequest) + { + TestContext?.WriteLine($" PR 1 - Review: {review}"); + Assert.IsTrue(review.PullRequestId == 1); + } + + testListener.PrintEventCounts(); + Assert.AreEqual(false, testListener.FoundErrors()); + } } diff --git a/test/GitHubExtension/DataStore/OctokitIngestionTests.cs b/test/GitHubExtension/DataStore/OctokitIngestionTests.cs index 64c26379..346f7d93 100644 --- a/test/GitHubExtension/DataStore/OctokitIngestionTests.cs +++ b/test/GitHubExtension/DataStore/OctokitIngestionTests.cs @@ -528,4 +528,53 @@ public void AddCheckRunFromOctokit() testListener.PrintEventCounts(); Assert.AreEqual(false, testListener.FoundErrors()); } + + [TestMethod] + [TestCategory("LiveData")] + public void AddReviewFromOctokit() + { + using var log = new DevHome.Logging.Logger("TestStore", TestOptions.LogOptions); + var testListener = new TestListener("TestListener", TestContext!); + log.AddListener(testListener); + Log.Attach(log); + + using var dataStore = new DataStore("TestStore", TestHelpers.GetDataStoreFilePath(TestOptions), TestOptions.DataStoreOptions.DataStoreSchema!); + Assert.IsNotNull(dataStore); + dataStore.Create(); + Assert.IsNotNull(dataStore.Connection); + + var client = GitHubClientProvider.Instance.GetClient(); + + using var tx = dataStore.Connection!.BeginTransaction(); + + var octoPull = client.PullRequest.Get("microsoft", "devhomegithubextension", 260).Result; + var dataStorePull = DataModel.PullRequest.GetOrCreateByOctokitPullRequest(dataStore, octoPull); + + // Get reviews for pull + var octoReviews = client.PullRequest.Review.GetAll("microsoft", "devhomegithubextension", 260).Result; + Assert.IsNotNull(octoReviews); + TestContext?.WriteLine($"Found {octoReviews.Count}"); + foreach (var review in octoReviews) + { + var dsReview = Review.GetOrCreateByOctokitReview(dataStore, review, dataStorePull.Id); + Assert.IsNotNull(dsReview); + } + + tx.Commit(); + + // Verify retrieval and input into data objects and we get the same results. + var dataStoreReviews = dataStore.Connection.GetAll().ToList(); + Assert.IsNotNull(dataStoreReviews); + var reviewsFromPull = dataStorePull.Reviews; + Assert.IsNotNull(reviewsFromPull); + Assert.AreEqual(dataStoreReviews.Count, reviewsFromPull.Count()); + + foreach (var review in reviewsFromPull) + { + TestContext?.WriteLine($" Id: {review.Id} PullId: {review.PullRequestId} InternalId: {review.InternalId} AuthorId: {review.AuthorId} Author:{review.Author.Login} State: {review.State} Submitted: {review.SubmittedAt}"); + } + + testListener.PrintEventCounts(); + Assert.AreEqual(false, testListener.FoundErrors()); + } }