From 12df8ef55878880b6d188f41d0c85c19b351863f Mon Sep 17 00:00:00 2001 From: Alitzel Mendez Date: Wed, 17 Sep 2025 19:08:03 -0700 Subject: [PATCH 1/4] Expose endpoints for MCP --- .../APIRevisionsTokenAuthController.cs | 20 ++++ .../LeanControllers/ReviewsController.cs | 66 ------------- .../ReviewsHybridAuthController.cs | 95 +++++++++++++++++++ 3 files changed, 115 insertions(+), 66 deletions(-) create mode 100644 src/dotnet/APIView/APIViewWeb/LeanControllers/ReviewsHybridAuthController.cs diff --git a/src/dotnet/APIView/APIViewWeb/LeanControllers/APIRevisionsTokenAuthController.cs b/src/dotnet/APIView/APIViewWeb/LeanControllers/APIRevisionsTokenAuthController.cs index 5cf66d7fd19..b1f010e46d7 100644 --- a/src/dotnet/APIView/APIViewWeb/LeanControllers/APIRevisionsTokenAuthController.cs +++ b/src/dotnet/APIView/APIViewWeb/LeanControllers/APIRevisionsTokenAuthController.cs @@ -134,4 +134,24 @@ public async Task> GetAPIRevisionTextAsync( return StatusCode(StatusCodes.Status500InternalServerError, "Failed to generate review text"); } } + + [HttpGet("{revisionId}/getReviewId", Name = "GetReviewId")] + public async Task> GetReviewIdForRevision(string revisionId) + { + try + { + APIRevisionListItemModel revision = await _apiRevisionsManager.GetAPIRevisionAsync(User, revisionId); + if (revision == null) + { + return NotFound($"No API revision found for ID: {revisionId}"); + } + + return new LeanJsonResult(revision.ReviewId, StatusCodes.Status200OK); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting review ID for revision {RevisionId}", revisionId); + return StatusCode(StatusCodes.Status500InternalServerError, "Failed to get review ID"); + } + } } diff --git a/src/dotnet/APIView/APIViewWeb/LeanControllers/ReviewsController.cs b/src/dotnet/APIView/APIViewWeb/LeanControllers/ReviewsController.cs index 88616dcc3d6..331368b731e 100644 --- a/src/dotnet/APIView/APIViewWeb/LeanControllers/ReviewsController.cs +++ b/src/dotnet/APIView/APIViewWeb/LeanControllers/ReviewsController.cs @@ -23,14 +23,11 @@ public class ReviewsController : BaseApiController { private readonly IReviewManager _reviewManager; private readonly IAPIRevisionsManager _apiRevisionsManager; - private readonly ICommentsManager _commentsManager; private readonly IBlobCodeFileRepository _codeFileRepository; private readonly IConfiguration _configuration; public readonly UserProfileCache _userProfileCache; private readonly IHubContext _signalRHubContext; private readonly INotificationManager _notificationManager; - private readonly IEnumerable _languageServices; - private readonly IWebHostEnvironment _env; public ReviewsController(ILogger logger, IAPIRevisionsManager reviewRevisionsManager, IReviewManager reviewManager, @@ -42,14 +39,11 @@ public ReviewsController(ILogger logger, { _apiRevisionsManager = reviewRevisionsManager; _reviewManager = reviewManager; - _commentsManager = commentManager; _codeFileRepository = codeFileRepository; _configuration = configuration; _userProfileCache = userProfileCache; - _languageServices = languageServices; _signalRHubContext = signalRHub; _notificationManager = notificationManager; - _env = env; } /// @@ -151,66 +145,6 @@ public async Task> ToggleSubscribeAsync(s return Ok(); } - /// - ///Retrieve the Content (codeLines and Navigation) of a review - /// - /// - /// - /// - /// - [Route("{reviewId}/content")] - [HttpGet] - public async Task> GetReviewContentAsync(string reviewId, [FromQuery] string activeApiRevisionId, - [FromQuery] string diffApiRevisionId = null) - { - var activeAPIRevision = await _apiRevisionsManager.GetAPIRevisionAsync(User, activeApiRevisionId); - APIRevisionListItemModel diffAPIRevision = null; - - if (activeAPIRevision.IsDeleted) - { - return new LeanJsonResult(null, StatusCodes.Status204NoContent); - } - - if (!string.IsNullOrEmpty(diffApiRevisionId)) - { - diffAPIRevision = await _apiRevisionsManager.GetAPIRevisionAsync(User, diffApiRevisionId); - - if (diffAPIRevision.IsDeleted) - { - return new LeanJsonResult(null, StatusCodes.Status204NoContent); - } - } - - if (activeAPIRevision.Files[0].ParserStyle == ParserStyle.Tree) - { - var comments = await _commentsManager.GetCommentsAsync(reviewId, commentType: CommentType.APIRevision); - var activeRevisionReviewCodeFile = await _codeFileRepository.GetCodeFileFromStorageAsync(revisionId: activeAPIRevision.Id, codeFileId: activeAPIRevision.Files[0].FileId); - - if (activeRevisionReviewCodeFile.ContentGenerationInProgress) - { - var languageServices = LanguageServiceHelpers.GetLanguageService(activeAPIRevision.Language, _languageServices); - return new LeanJsonResult("Content generation in progress", StatusCodes.Status202Accepted, languageServices.ReviewGenerationPipelineUrl); - } - - var codePanelRawData = new CodePanelRawData() - { - activeRevisionCodeFile = activeRevisionReviewCodeFile, - Comments = comments - }; - - if (diffAPIRevision != null) - { - codePanelRawData.diffRevisionCodeFile = await _codeFileRepository.GetCodeFileFromStorageAsync(revisionId: diffAPIRevision.Id, codeFileId: diffAPIRevision.Files[0].FileId); - } - - // Render the code files to generate UI token tree - var result = await CodeFileHelpers.GenerateCodePanelDataAsync(codePanelRawData); - return new LeanJsonResult(result, StatusCodes.Status200OK); - } - - return new LeanJsonResult("Invalid APIRevision", StatusCodes.Status500InternalServerError); - } - /// ///Retrieve Cross Language Content for specified revisions /// diff --git a/src/dotnet/APIView/APIViewWeb/LeanControllers/ReviewsHybridAuthController.cs b/src/dotnet/APIView/APIViewWeb/LeanControllers/ReviewsHybridAuthController.cs new file mode 100644 index 00000000000..21b844ffd24 --- /dev/null +++ b/src/dotnet/APIView/APIViewWeb/LeanControllers/ReviewsHybridAuthController.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using APIViewWeb.Helpers; +using APIViewWeb.LeanModels; +using APIViewWeb.Managers; +using APIViewWeb.Managers.Interfaces; +using APIViewWeb.Repositories; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace APIViewWeb.LeanControllers; + +[ApiController] +[Authorize("RequireTokenOrCookieAuthentication")] +[Route("api/reviews")] +public class ReviewsHybridAuthController : ControllerBase +{ + private readonly ICommentsManager _commentsManager; + private readonly IAPIRevisionsManager _apiRevisionsManager; + private readonly IBlobCodeFileRepository _codeFileRepository; + private readonly IEnumerable _languageServices; + + public ReviewsHybridAuthController(ICommentsManager commentsManager, + IBlobCodeFileRepository codeFileRepository, + IAPIRevisionsManager reviewRevisionsManager, + IEnumerable languageServices) + { + _apiRevisionsManager = reviewRevisionsManager; + _codeFileRepository = codeFileRepository; + _commentsManager = commentsManager; + _languageServices = languageServices; + } + + /// + ///Retrieve the Content (codeLines and Navigation) of a review + /// + /// + /// + /// + /// + [Route("{reviewId}/content")] + [HttpGet] + public async Task> GetReviewContentAsync(string reviewId, [FromQuery] string activeApiRevisionId, + [FromQuery] string diffApiRevisionId = null) + { + var activeAPIRevision = await _apiRevisionsManager.GetAPIRevisionAsync(User, activeApiRevisionId); + APIRevisionListItemModel diffAPIRevision = null; + + if (activeAPIRevision.IsDeleted) + { + return new LeanJsonResult(null, StatusCodes.Status204NoContent); + } + + if (!string.IsNullOrEmpty(diffApiRevisionId)) + { + diffAPIRevision = await _apiRevisionsManager.GetAPIRevisionAsync(User, diffApiRevisionId); + + if (diffAPIRevision.IsDeleted) + { + return new LeanJsonResult(null, StatusCodes.Status204NoContent); + } + } + + if (activeAPIRevision.Files[0].ParserStyle == ParserStyle.Tree) + { + var comments = await _commentsManager.GetCommentsAsync(reviewId, commentType: CommentType.APIRevision); + var activeRevisionReviewCodeFile = await _codeFileRepository.GetCodeFileFromStorageAsync(revisionId: activeAPIRevision.Id, codeFileId: activeAPIRevision.Files[0].FileId); + + if (activeRevisionReviewCodeFile.ContentGenerationInProgress) + { + var languageServices = LanguageServiceHelpers.GetLanguageService(activeAPIRevision.Language, _languageServices); + return new LeanJsonResult("Content generation in progress", StatusCodes.Status202Accepted, languageServices.ReviewGenerationPipelineUrl); + } + + var codePanelRawData = new CodePanelRawData() + { + activeRevisionCodeFile = activeRevisionReviewCodeFile, + Comments = comments + }; + + if (diffAPIRevision != null) + { + codePanelRawData.diffRevisionCodeFile = await _codeFileRepository.GetCodeFileFromStorageAsync(revisionId: diffAPIRevision.Id, codeFileId: diffAPIRevision.Files[0].FileId); + } + + // Render the code files to generate UI token tree + var result = await CodeFileHelpers.GenerateCodePanelDataAsync(codePanelRawData); + return new LeanJsonResult(result, StatusCodes.Status200OK); + } + + return new LeanJsonResult("Invalid APIRevision", StatusCodes.Status500InternalServerError); + } + +} From 3f69866d87e77fd0789b154cd2955abbca9b809b Mon Sep 17 00:00:00 2001 From: Alitzel Mendez Date: Wed, 17 Sep 2025 19:29:59 -0700 Subject: [PATCH 2/4] copilot feedback --- .../LeanControllers/APIRevisionsTokenAuthController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dotnet/APIView/APIViewWeb/LeanControllers/APIRevisionsTokenAuthController.cs b/src/dotnet/APIView/APIViewWeb/LeanControllers/APIRevisionsTokenAuthController.cs index b1f010e46d7..c1c37393a4f 100644 --- a/src/dotnet/APIView/APIViewWeb/LeanControllers/APIRevisionsTokenAuthController.cs +++ b/src/dotnet/APIView/APIViewWeb/LeanControllers/APIRevisionsTokenAuthController.cs @@ -136,7 +136,7 @@ public async Task> GetAPIRevisionTextAsync( } [HttpGet("{revisionId}/getReviewId", Name = "GetReviewId")] - public async Task> GetReviewIdForRevision(string revisionId) + public async Task GetReviewIdForRevision(string revisionId) { try { From 11d176d31ce4d42b65aa61248b11bb24b73fa086 Mon Sep 17 00:00:00 2001 From: Alitzel Mendez Date: Thu, 18 Sep 2025 13:40:42 -0700 Subject: [PATCH 3/4] feedback --- .../APIRevisionsTokenAuthController.cs | 124 +++++++++--------- .../LeanControllers/ReviewsController.cs | 66 ++++++++++ .../ReviewsHybridAuthController.cs | 95 -------------- .../Models/APIRevisionSelectionType.cs | 5 + 4 files changed, 132 insertions(+), 158 deletions(-) delete mode 100644 src/dotnet/APIView/APIViewWeb/LeanControllers/ReviewsHybridAuthController.cs diff --git a/src/dotnet/APIView/APIViewWeb/LeanControllers/APIRevisionsTokenAuthController.cs b/src/dotnet/APIView/APIViewWeb/LeanControllers/APIRevisionsTokenAuthController.cs index c1c37393a4f..9c65abdfdc4 100644 --- a/src/dotnet/APIView/APIViewWeb/LeanControllers/APIRevisionsTokenAuthController.cs +++ b/src/dotnet/APIView/APIViewWeb/LeanControllers/APIRevisionsTokenAuthController.cs @@ -2,10 +2,12 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using ApiView; using APIViewWeb.Helpers; using APIViewWeb.LeanModels; using APIViewWeb.Managers.Interfaces; using APIViewWeb.Models; +using APIViewWeb.Repositories; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -19,14 +21,16 @@ namespace APIViewWeb.LeanControllers; public class APIRevisionsTokenAuthController : ControllerBase { private readonly IAPIRevisionsManager _apiRevisionsManager; - + private readonly IBlobCodeFileRepository _codeFileRepository; private readonly ILogger _logger; - public APIRevisionsTokenAuthController(ILogger logger, - IAPIRevisionsManager apiRevisionsManager) + public APIRevisionsTokenAuthController(IBlobCodeFileRepository codeFileRepository, + IAPIRevisionsManager apiRevisionsManager, + ILogger logger) { - _logger = logger; _apiRevisionsManager = apiRevisionsManager; + _codeFileRepository = codeFileRepository; + _logger = logger; } /// @@ -53,13 +57,15 @@ public async Task> GetOutlineAPIRevisionA /// Generate review text for an API revision /// /// The review ID + /// The content return type. Default is text, but codefile can also be selected /// The specific API revision ID (required when there is not selectionType) /// How to select the API revision /// Plain text representation of the API review - [HttpGet("getRevisionText", Name = "GetAPIRevisionText")] - public async Task> GetAPIRevisionTextAsync( + [HttpGet("getRevisionContent", Name = "GetAPIRevisionContent")] + public async Task GetAPIRevisionTextAsync( [FromQuery] string apiRevisionId = null, [FromQuery] string reviewId = null, + [FromQuery] APIRevisionContentReturnType contentReturnType = APIRevisionContentReturnType.Text, [FromQuery] APIRevisionSelectionType selectionType = APIRevisionSelectionType.Undefined) { try @@ -74,57 +80,35 @@ public async Task> GetAPIRevisionTextAsync( return BadRequest($"reviewId is required when selectionType is {selectionType}"); } - APIRevisionListItemModel activeApiRevision = null; - switch (selectionType) - { - case APIRevisionSelectionType.Undefined: - activeApiRevision = await _apiRevisionsManager.GetAPIRevisionAsync(User, apiRevisionId); - break; - case APIRevisionSelectionType.Latest: - activeApiRevision = await _apiRevisionsManager.GetLatestAPIRevisionsAsync(reviewId); - break; - case APIRevisionSelectionType.LatestApproved: - IEnumerable allRevisions = - await _apiRevisionsManager.GetAPIRevisionsAsync(reviewId); - activeApiRevision = allRevisions - .Where(r => r.IsApproved && !r.IsDeleted) - .OrderByDescending(r => r.CreatedOn) - .FirstOrDefault(); - break; - case APIRevisionSelectionType.LatestAutomatic: - activeApiRevision = await _apiRevisionsManager.GetLatestAPIRevisionsAsync( - reviewId, - apiRevisionType: APIRevisionType.Automatic); - break; - case APIRevisionSelectionType.LatestManual: - activeApiRevision = await _apiRevisionsManager.GetLatestAPIRevisionsAsync( - reviewId, - apiRevisionType: APIRevisionType.Manual); - break; - - default: - return BadRequest($"Unsupported selection type: {selectionType}"); - } - - if (activeApiRevision == null) - { - return NotFound($"No API revision found for selection type: {selectionType}"); - } - - if (activeApiRevision.IsDeleted) + APIRevisionListItemModel activeApiRevision = await GetApiRevisionBySelectionType(selectionType, reviewId, apiRevisionId); + if (activeApiRevision == null || activeApiRevision.IsDeleted) { return new LeanJsonResult(null, StatusCodes.Status204NoContent); } - if ((selectionType == APIRevisionSelectionType.Undefined && !string.IsNullOrEmpty(reviewId) && activeApiRevision.ReviewId != reviewId) || - (selectionType != APIRevisionSelectionType.Undefined && !string.IsNullOrEmpty(apiRevisionId) && activeApiRevision.Id != apiRevisionId)) + if (IsValidateRevisionMatch(activeApiRevision, reviewId, apiRevisionId, selectionType)) { return BadRequest( $"Mismatch between reviewId and apiRevisionId: The API revision '{apiRevisionId}' does not belong to review '{reviewId}'. Ensure the revision ID corresponds to the specified review."); } - string reviewText = await _apiRevisionsManager.GetApiRevisionText(activeApiRevision); - return new LeanJsonResult(reviewText, StatusCodes.Status200OK); + switch (contentReturnType) + { + case APIRevisionContentReturnType.Text: + string reviewText = await _apiRevisionsManager.GetApiRevisionText(activeApiRevision); + return new LeanJsonResult(reviewText, StatusCodes.Status200OK); + case APIRevisionContentReturnType.CodeFile: + CodeFile activeRevisionReviewCodeFile = await _codeFileRepository.GetCodeFileFromStorageAsync(activeApiRevision.Id, activeApiRevision.Files[0].FileId); + if (activeRevisionReviewCodeFile == null) + { + return NotFound($"No code file found for API revision ID: {activeApiRevision.Id}"); + } + + return new LeanJsonResult(activeRevisionReviewCodeFile, StatusCodes.Status200OK); + default: + return BadRequest( + $"Unsupported contentReturnType: {contentReturnType}. Supported are {APIRevisionContentReturnType.Text} | {APIRevisionContentReturnType.CodeFile}"); + } } catch (Exception ex) { @@ -135,23 +119,37 @@ public async Task> GetAPIRevisionTextAsync( } } - [HttpGet("{revisionId}/getReviewId", Name = "GetReviewId")] - public async Task GetReviewIdForRevision(string revisionId) + private async Task GetApiRevisionBySelectionType( + APIRevisionSelectionType selectionType, string reviewId, string apiRevisionId) { - try + return selectionType switch { - APIRevisionListItemModel revision = await _apiRevisionsManager.GetAPIRevisionAsync(User, revisionId); - if (revision == null) - { - return NotFound($"No API revision found for ID: {revisionId}"); - } + APIRevisionSelectionType.Undefined => await _apiRevisionsManager.GetAPIRevisionAsync(User, apiRevisionId), + APIRevisionSelectionType.Latest => await _apiRevisionsManager.GetLatestAPIRevisionsAsync(reviewId), + APIRevisionSelectionType.LatestApproved => await GetLatestApprovedRevision(reviewId), + APIRevisionSelectionType.LatestAutomatic => await _apiRevisionsManager.GetLatestAPIRevisionsAsync(reviewId, apiRevisionType: APIRevisionType.Automatic), + APIRevisionSelectionType.LatestManual => await _apiRevisionsManager.GetLatestAPIRevisionsAsync(reviewId, apiRevisionType: APIRevisionType.Manual), + _ => throw new ArgumentException($"Unsupported selection type: {selectionType}") + }; + } - return new LeanJsonResult(revision.ReviewId, StatusCodes.Status200OK); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting review ID for revision {RevisionId}", revisionId); - return StatusCode(StatusCodes.Status500InternalServerError, "Failed to get review ID"); - } + private async Task GetLatestApprovedRevision(string reviewId) + { + IEnumerable allRevisions = await _apiRevisionsManager.GetAPIRevisionsAsync(reviewId); + return allRevisions + .Where(r => r.IsApproved && !r.IsDeleted) + .OrderByDescending(r => r.CreatedOn) + .FirstOrDefault(); + } + + private bool IsValidateRevisionMatch(APIRevisionListItemModel activeApiRevision, string reviewId, string apiRevisionId, APIRevisionSelectionType selectionType) + { + bool hasReviewIdMismatch = selectionType == APIRevisionSelectionType.Undefined && + !string.IsNullOrEmpty(reviewId) && activeApiRevision.ReviewId != reviewId; + + bool hasRevisionIdMismatch = selectionType != APIRevisionSelectionType.Undefined && + !string.IsNullOrEmpty(apiRevisionId) && activeApiRevision.Id != apiRevisionId; + + return hasRevisionIdMismatch || hasReviewIdMismatch; } } diff --git a/src/dotnet/APIView/APIViewWeb/LeanControllers/ReviewsController.cs b/src/dotnet/APIView/APIViewWeb/LeanControllers/ReviewsController.cs index 331368b731e..88616dcc3d6 100644 --- a/src/dotnet/APIView/APIViewWeb/LeanControllers/ReviewsController.cs +++ b/src/dotnet/APIView/APIViewWeb/LeanControllers/ReviewsController.cs @@ -23,11 +23,14 @@ public class ReviewsController : BaseApiController { private readonly IReviewManager _reviewManager; private readonly IAPIRevisionsManager _apiRevisionsManager; + private readonly ICommentsManager _commentsManager; private readonly IBlobCodeFileRepository _codeFileRepository; private readonly IConfiguration _configuration; public readonly UserProfileCache _userProfileCache; private readonly IHubContext _signalRHubContext; private readonly INotificationManager _notificationManager; + private readonly IEnumerable _languageServices; + private readonly IWebHostEnvironment _env; public ReviewsController(ILogger logger, IAPIRevisionsManager reviewRevisionsManager, IReviewManager reviewManager, @@ -39,11 +42,14 @@ public ReviewsController(ILogger logger, { _apiRevisionsManager = reviewRevisionsManager; _reviewManager = reviewManager; + _commentsManager = commentManager; _codeFileRepository = codeFileRepository; _configuration = configuration; _userProfileCache = userProfileCache; + _languageServices = languageServices; _signalRHubContext = signalRHub; _notificationManager = notificationManager; + _env = env; } /// @@ -145,6 +151,66 @@ public async Task> ToggleSubscribeAsync(s return Ok(); } + /// + ///Retrieve the Content (codeLines and Navigation) of a review + /// + /// + /// + /// + /// + [Route("{reviewId}/content")] + [HttpGet] + public async Task> GetReviewContentAsync(string reviewId, [FromQuery] string activeApiRevisionId, + [FromQuery] string diffApiRevisionId = null) + { + var activeAPIRevision = await _apiRevisionsManager.GetAPIRevisionAsync(User, activeApiRevisionId); + APIRevisionListItemModel diffAPIRevision = null; + + if (activeAPIRevision.IsDeleted) + { + return new LeanJsonResult(null, StatusCodes.Status204NoContent); + } + + if (!string.IsNullOrEmpty(diffApiRevisionId)) + { + diffAPIRevision = await _apiRevisionsManager.GetAPIRevisionAsync(User, diffApiRevisionId); + + if (diffAPIRevision.IsDeleted) + { + return new LeanJsonResult(null, StatusCodes.Status204NoContent); + } + } + + if (activeAPIRevision.Files[0].ParserStyle == ParserStyle.Tree) + { + var comments = await _commentsManager.GetCommentsAsync(reviewId, commentType: CommentType.APIRevision); + var activeRevisionReviewCodeFile = await _codeFileRepository.GetCodeFileFromStorageAsync(revisionId: activeAPIRevision.Id, codeFileId: activeAPIRevision.Files[0].FileId); + + if (activeRevisionReviewCodeFile.ContentGenerationInProgress) + { + var languageServices = LanguageServiceHelpers.GetLanguageService(activeAPIRevision.Language, _languageServices); + return new LeanJsonResult("Content generation in progress", StatusCodes.Status202Accepted, languageServices.ReviewGenerationPipelineUrl); + } + + var codePanelRawData = new CodePanelRawData() + { + activeRevisionCodeFile = activeRevisionReviewCodeFile, + Comments = comments + }; + + if (diffAPIRevision != null) + { + codePanelRawData.diffRevisionCodeFile = await _codeFileRepository.GetCodeFileFromStorageAsync(revisionId: diffAPIRevision.Id, codeFileId: diffAPIRevision.Files[0].FileId); + } + + // Render the code files to generate UI token tree + var result = await CodeFileHelpers.GenerateCodePanelDataAsync(codePanelRawData); + return new LeanJsonResult(result, StatusCodes.Status200OK); + } + + return new LeanJsonResult("Invalid APIRevision", StatusCodes.Status500InternalServerError); + } + /// ///Retrieve Cross Language Content for specified revisions /// diff --git a/src/dotnet/APIView/APIViewWeb/LeanControllers/ReviewsHybridAuthController.cs b/src/dotnet/APIView/APIViewWeb/LeanControllers/ReviewsHybridAuthController.cs deleted file mode 100644 index 21b844ffd24..00000000000 --- a/src/dotnet/APIView/APIViewWeb/LeanControllers/ReviewsHybridAuthController.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using APIViewWeb.Helpers; -using APIViewWeb.LeanModels; -using APIViewWeb.Managers; -using APIViewWeb.Managers.Interfaces; -using APIViewWeb.Repositories; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace APIViewWeb.LeanControllers; - -[ApiController] -[Authorize("RequireTokenOrCookieAuthentication")] -[Route("api/reviews")] -public class ReviewsHybridAuthController : ControllerBase -{ - private readonly ICommentsManager _commentsManager; - private readonly IAPIRevisionsManager _apiRevisionsManager; - private readonly IBlobCodeFileRepository _codeFileRepository; - private readonly IEnumerable _languageServices; - - public ReviewsHybridAuthController(ICommentsManager commentsManager, - IBlobCodeFileRepository codeFileRepository, - IAPIRevisionsManager reviewRevisionsManager, - IEnumerable languageServices) - { - _apiRevisionsManager = reviewRevisionsManager; - _codeFileRepository = codeFileRepository; - _commentsManager = commentsManager; - _languageServices = languageServices; - } - - /// - ///Retrieve the Content (codeLines and Navigation) of a review - /// - /// - /// - /// - /// - [Route("{reviewId}/content")] - [HttpGet] - public async Task> GetReviewContentAsync(string reviewId, [FromQuery] string activeApiRevisionId, - [FromQuery] string diffApiRevisionId = null) - { - var activeAPIRevision = await _apiRevisionsManager.GetAPIRevisionAsync(User, activeApiRevisionId); - APIRevisionListItemModel diffAPIRevision = null; - - if (activeAPIRevision.IsDeleted) - { - return new LeanJsonResult(null, StatusCodes.Status204NoContent); - } - - if (!string.IsNullOrEmpty(diffApiRevisionId)) - { - diffAPIRevision = await _apiRevisionsManager.GetAPIRevisionAsync(User, diffApiRevisionId); - - if (diffAPIRevision.IsDeleted) - { - return new LeanJsonResult(null, StatusCodes.Status204NoContent); - } - } - - if (activeAPIRevision.Files[0].ParserStyle == ParserStyle.Tree) - { - var comments = await _commentsManager.GetCommentsAsync(reviewId, commentType: CommentType.APIRevision); - var activeRevisionReviewCodeFile = await _codeFileRepository.GetCodeFileFromStorageAsync(revisionId: activeAPIRevision.Id, codeFileId: activeAPIRevision.Files[0].FileId); - - if (activeRevisionReviewCodeFile.ContentGenerationInProgress) - { - var languageServices = LanguageServiceHelpers.GetLanguageService(activeAPIRevision.Language, _languageServices); - return new LeanJsonResult("Content generation in progress", StatusCodes.Status202Accepted, languageServices.ReviewGenerationPipelineUrl); - } - - var codePanelRawData = new CodePanelRawData() - { - activeRevisionCodeFile = activeRevisionReviewCodeFile, - Comments = comments - }; - - if (diffAPIRevision != null) - { - codePanelRawData.diffRevisionCodeFile = await _codeFileRepository.GetCodeFileFromStorageAsync(revisionId: diffAPIRevision.Id, codeFileId: diffAPIRevision.Files[0].FileId); - } - - // Render the code files to generate UI token tree - var result = await CodeFileHelpers.GenerateCodePanelDataAsync(codePanelRawData); - return new LeanJsonResult(result, StatusCodes.Status200OK); - } - - return new LeanJsonResult("Invalid APIRevision", StatusCodes.Status500InternalServerError); - } - -} diff --git a/src/dotnet/APIView/APIViewWeb/Models/APIRevisionSelectionType.cs b/src/dotnet/APIView/APIViewWeb/Models/APIRevisionSelectionType.cs index 70c094ea16f..6bd4289a1fd 100644 --- a/src/dotnet/APIView/APIViewWeb/Models/APIRevisionSelectionType.cs +++ b/src/dotnet/APIView/APIViewWeb/Models/APIRevisionSelectionType.cs @@ -23,5 +23,10 @@ public enum APIRevisionSelectionType /// Use the latest manual revision (non-automatic) /// LatestManual = 4 +} +public enum APIRevisionContentReturnType +{ + Text, + CodeFile } From ad84d5b76ee0358b1a86e6ac27e16ce9fe2589ab Mon Sep 17 00:00:00 2001 From: Alitzel Mendez Date: Thu, 18 Sep 2025 15:04:47 -0700 Subject: [PATCH 4/4] fix and add tests --- .../APIRevisionsControllerTests.cs | 53 ++++++++++++++----- .../APIRevisionsTokenAuthController.cs | 10 ++-- 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/src/dotnet/APIView/APIViewUnitTests/APIRevisionsControllerTests.cs b/src/dotnet/APIView/APIViewUnitTests/APIRevisionsControllerTests.cs index 497ae85fe46..d75c432025c 100644 --- a/src/dotnet/APIView/APIViewUnitTests/APIRevisionsControllerTests.cs +++ b/src/dotnet/APIView/APIViewUnitTests/APIRevisionsControllerTests.cs @@ -4,6 +4,8 @@ using System.Net.Http; using System.Security.Claims; using System.Threading.Tasks; +using ApiView; +using APIViewWeb; using APIViewWeb.Helpers; using APIViewWeb.Hubs; using APIViewWeb.LeanControllers; @@ -25,6 +27,7 @@ namespace APIViewUnitTests; public class APIRevisionsControllerTests { private readonly APIRevisionsTokenAuthController _controller; + private readonly Mock _mockBlobCodeFileRepository; private readonly Mock _mockApiRevisionsManager; private readonly Mock> _mockLogger; @@ -32,6 +35,7 @@ public APIRevisionsControllerTests() { _mockLogger = new Mock>(); _mockApiRevisionsManager = new Mock(); + _mockBlobCodeFileRepository = new Mock(); Mock mockReviewManager = new(); Mock mockNotificationManager = new(); @@ -41,8 +45,9 @@ public APIRevisionsControllerTests() Mock mockPullRequestManager = new(); _controller = new APIRevisionsTokenAuthController( - _mockLogger.Object, - _mockApiRevisionsManager.Object + _mockBlobCodeFileRepository.Object, + _mockApiRevisionsManager.Object, + _mockLogger.Object ); List claims = new() { new Claim("login", "testuser") }; @@ -71,7 +76,7 @@ public async Task GetAPIRevisionTextAsync_WithValidId_ReturnsRevisionText() .Setup(x => x.GetApiRevisionText(expectedRevision)) .ReturnsAsync(expectedText); - ActionResult result = await _controller.GetAPIRevisionTextAsync(apiRevisionId, reviewId); + ActionResult result = await _controller.GetAPIRevisionContentAsync(apiRevisionId, reviewId); LeanJsonResult actionResult = Assert.IsType(result.Result); Assert.Equal(expectedText, actionResult.Value); @@ -80,11 +85,31 @@ public async Task GetAPIRevisionTextAsync_WithValidId_ReturnsRevisionText() _mockApiRevisionsManager.Verify(x => x.GetApiRevisionText(expectedRevision), Times.Once); } + [Fact] + public async Task GetAPIRevisionCodeFileAsync_WithValidId_ReturnsCodeFile() + { + string reviewId = "review123"; + string apiRevisionId = "revision456"; + + APIRevisionListItemModel expectedRevision = CreateMockAPIRevision(apiRevisionId); + + _mockApiRevisionsManager + .Setup(x => x.GetAPIRevisionAsync(It.IsAny(), apiRevisionId)) + .ReturnsAsync(expectedRevision); + + _mockBlobCodeFileRepository.Setup(x => x.GetCodeFileFromStorageAsync(It.IsAny(), It.IsAny())).ReturnsAsync(new CodeFile()); + + await _controller.GetAPIRevisionContentAsync(apiRevisionId, reviewId, contentReturnType: APIRevisionContentReturnType.CodeFile); + _mockApiRevisionsManager.Verify(x => x.GetAPIRevisionAsync(It.IsAny(), apiRevisionId), + Times.Once); + _mockBlobCodeFileRepository.Verify(x => x.GetCodeFileFromStorageAsync(It.IsAny(), It.IsAny())); + } + [Fact] public async Task GetAPIRevisionTextAsync_WithOnlyReviewId_ReturnsBadRequest() { string reviewId = "review123"; - ActionResult result = await _controller.GetAPIRevisionTextAsync(null, reviewId); + ActionResult result = await _controller.GetAPIRevisionContentAsync(null, reviewId); BadRequestObjectResult badRequestResult = Assert.IsType(result.Result); Assert.Equal("apiRevisionId is required", badRequestResult.Value); @@ -100,10 +125,10 @@ public async Task GetAPIRevisionTextAsync_WithDeletedRevision_ReturnsNoContent() .Setup(x => x.GetAPIRevisionAsync(It.IsAny(), apiRevisionId)) .ReturnsAsync(deletedRevision); - ActionResult result = await _controller.GetAPIRevisionTextAsync(apiRevisionId); + ActionResult result = await _controller.GetAPIRevisionContentAsync(apiRevisionId); - LeanJsonResult actionResult = Assert.IsType(result.Result); - Assert.Null(actionResult.Value); + NotFoundObjectResult notFoundResult = Assert.IsType(result.Result); + Assert.Equal("No API revision found for selection type: Undefined", notFoundResult.Value); } [Fact] @@ -121,7 +146,7 @@ public async Task GetAPIRevisionTextAsync_LatestType_ReturnsLatestRevision() .Setup(x => x.GetApiRevisionText(expectedRevision)) .ReturnsAsync(expectedText); - ActionResult result = await _controller.GetAPIRevisionTextAsync( + ActionResult result = await _controller.GetAPIRevisionContentAsync( null, reviewId, APIRevisionSelectionType.Latest); @@ -156,7 +181,7 @@ public async Task GetAPIRevisionTextAsync_LatestApprovedType_ReturnsLatestApprov .Setup(x => x.GetApiRevisionText(expectedRevision)) .ReturnsAsync(expectedText); - ActionResult result = await _controller.GetAPIRevisionTextAsync( + ActionResult result = await _controller.GetAPIRevisionContentAsync( null, reviewId, APIRevisionSelectionType.LatestApproved); @@ -181,7 +206,7 @@ public async Task GetAPIRevisionTextAsync_LatestApprovedType_NoApprovedRevisions .Setup(x => x.GetAPIRevisionsAsync(reviewId, "", APIRevisionType.All)) .ReturnsAsync(allRevisions); - ActionResult result = await _controller.GetAPIRevisionTextAsync(null, reviewId, APIRevisionSelectionType.LatestApproved); + ActionResult result = await _controller.GetAPIRevisionContentAsync(null, reviewId, APIRevisionSelectionType.LatestApproved); NotFoundObjectResult notFoundResult = Assert.IsType(result.Result); Assert.Equal("No API revision found for selection type: LatestApproved", notFoundResult.Value); @@ -202,7 +227,7 @@ public async Task GetAPIRevisionTextAsync_LatestManualType_ReturnsLatestManualRe .Setup(x => x.GetApiRevisionText(expectedRevision)) .ReturnsAsync(expectedText); - ActionResult result = await _controller.GetAPIRevisionTextAsync( + ActionResult result = await _controller.GetAPIRevisionContentAsync( null, reviewId, APIRevisionSelectionType.LatestManual); @@ -222,7 +247,7 @@ public async Task GetAPIRevisionTextAsync_LatestType_NoRevisionFound_ReturnsNotF .Setup(x => x.GetLatestAPIRevisionsAsync(reviewId, null, APIRevisionType.All)) .ReturnsAsync((APIRevisionListItemModel)null); - ActionResult result = await _controller.GetAPIRevisionTextAsync( + ActionResult result = await _controller.GetAPIRevisionContentAsync( null, reviewId, APIRevisionSelectionType.Latest); @@ -240,7 +265,7 @@ public async Task GetAPIRevisionTextAsync_ExceptionThrown_ReturnsInternalServerE .Setup(x => x.GetAPIRevisionAsync(It.IsAny(), apiRevisionId)) .ThrowsAsync(new Exception("Database error")); - ActionResult result = await _controller.GetAPIRevisionTextAsync( + ActionResult result = await _controller.GetAPIRevisionContentAsync( apiRevisionId); ObjectResult statusCodeResult = Assert.IsType(result.Result); @@ -261,7 +286,7 @@ private static APIRevisionListItemModel CreateMockAPIRevision( IsApproved = isApproved, CreatedOn = createdOn ?? DateTime.UtcNow, ReviewId = "review123", - Files = [], + Files = [new APICodeFileModel { FileId = "1" }], ChangeHistory = [], Approvers = [], ViewedBy = [], diff --git a/src/dotnet/APIView/APIViewWeb/LeanControllers/APIRevisionsTokenAuthController.cs b/src/dotnet/APIView/APIViewWeb/LeanControllers/APIRevisionsTokenAuthController.cs index 9c65abdfdc4..06f85891856 100644 --- a/src/dotnet/APIView/APIViewWeb/LeanControllers/APIRevisionsTokenAuthController.cs +++ b/src/dotnet/APIView/APIViewWeb/LeanControllers/APIRevisionsTokenAuthController.cs @@ -57,16 +57,16 @@ public async Task> GetOutlineAPIRevisionA /// Generate review text for an API revision /// /// The review ID - /// The content return type. Default is text, but codefile can also be selected /// The specific API revision ID (required when there is not selectionType) /// How to select the API revision + /// The content return type. Default is text, but CodeFile can also be selected /// Plain text representation of the API review [HttpGet("getRevisionContent", Name = "GetAPIRevisionContent")] - public async Task GetAPIRevisionTextAsync( + public async Task GetAPIRevisionContentAsync( [FromQuery] string apiRevisionId = null, [FromQuery] string reviewId = null, - [FromQuery] APIRevisionContentReturnType contentReturnType = APIRevisionContentReturnType.Text, - [FromQuery] APIRevisionSelectionType selectionType = APIRevisionSelectionType.Undefined) + [FromQuery] APIRevisionSelectionType selectionType = APIRevisionSelectionType.Undefined, + [FromQuery] APIRevisionContentReturnType contentReturnType = APIRevisionContentReturnType.Text) { try { @@ -83,7 +83,7 @@ public async Task GetAPIRevisionTextAsync( APIRevisionListItemModel activeApiRevision = await GetApiRevisionBySelectionType(selectionType, reviewId, apiRevisionId); if (activeApiRevision == null || activeApiRevision.IsDeleted) { - return new LeanJsonResult(null, StatusCodes.Status204NoContent); + return NotFound($"No API revision found for selection type: {selectionType}"); } if (IsValidateRevisionMatch(activeApiRevision, reviewId, apiRevisionId, selectionType))