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 5cf66d7fd19..06f85891856 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; } /// @@ -55,12 +59,14 @@ public async Task> GetOutlineAPIRevisionA /// The review ID /// 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("getRevisionText", Name = "GetAPIRevisionText")] - public async Task> GetAPIRevisionTextAsync( + [HttpGet("getRevisionContent", Name = "GetAPIRevisionContent")] + public async Task GetAPIRevisionContentAsync( [FromQuery] string apiRevisionId = null, [FromQuery] string reviewId = null, - [FromQuery] APIRevisionSelectionType selectionType = APIRevisionSelectionType.Undefined) + [FromQuery] APIRevisionSelectionType selectionType = APIRevisionSelectionType.Undefined, + [FromQuery] APIRevisionContentReturnType contentReturnType = APIRevisionContentReturnType.Text) { 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) + APIRevisionListItemModel activeApiRevision = await GetApiRevisionBySelectionType(selectionType, reviewId, apiRevisionId); + if (activeApiRevision == null || activeApiRevision.IsDeleted) { return NotFound($"No API revision found for selection type: {selectionType}"); } - if (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) { @@ -134,4 +118,38 @@ public async Task> GetAPIRevisionTextAsync( return StatusCode(StatusCodes.Status500InternalServerError, "Failed to generate review text"); } } + + private async Task GetApiRevisionBySelectionType( + APIRevisionSelectionType selectionType, string reviewId, string apiRevisionId) + { + return selectionType switch + { + 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}") + }; + } + + 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/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 }