Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,13 +27,15 @@ namespace APIViewUnitTests;
public class APIRevisionsControllerTests
{
private readonly APIRevisionsTokenAuthController _controller;
private readonly Mock<IBlobCodeFileRepository> _mockBlobCodeFileRepository;
private readonly Mock<IAPIRevisionsManager> _mockApiRevisionsManager;
private readonly Mock<ILogger<APIRevisionsTokenAuthController>> _mockLogger;

public APIRevisionsControllerTests()
{
_mockLogger = new Mock<ILogger<APIRevisionsTokenAuthController>>();
_mockApiRevisionsManager = new Mock<IAPIRevisionsManager>();
_mockBlobCodeFileRepository = new Mock<IBlobCodeFileRepository>();

Mock<IReviewManager> mockReviewManager = new();
Mock<INotificationManager> mockNotificationManager = new();
Expand All @@ -41,8 +45,9 @@ public APIRevisionsControllerTests()
Mock<IPullRequestManager> mockPullRequestManager = new();

_controller = new APIRevisionsTokenAuthController(
_mockLogger.Object,
_mockApiRevisionsManager.Object
_mockBlobCodeFileRepository.Object,
_mockApiRevisionsManager.Object,
_mockLogger.Object
);

List<Claim> claims = new() { new Claim("login", "testuser") };
Expand Down Expand Up @@ -71,7 +76,7 @@ public async Task GetAPIRevisionTextAsync_WithValidId_ReturnsRevisionText()
.Setup(x => x.GetApiRevisionText(expectedRevision))
.ReturnsAsync(expectedText);

ActionResult<string> result = await _controller.GetAPIRevisionTextAsync(apiRevisionId, reviewId);
ActionResult<string> result = await _controller.GetAPIRevisionContentAsync(apiRevisionId, reviewId);

LeanJsonResult actionResult = Assert.IsType<LeanJsonResult>(result.Result);
Assert.Equal(expectedText, actionResult.Value);
Expand All @@ -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<ClaimsPrincipal>(), apiRevisionId))
.ReturnsAsync(expectedRevision);

_mockBlobCodeFileRepository.Setup(x => x.GetCodeFileFromStorageAsync(It.IsAny<string>(), It.IsAny<string>())).ReturnsAsync(new CodeFile());

await _controller.GetAPIRevisionContentAsync(apiRevisionId, reviewId, contentReturnType: APIRevisionContentReturnType.CodeFile);
_mockApiRevisionsManager.Verify(x => x.GetAPIRevisionAsync(It.IsAny<ClaimsPrincipal>(), apiRevisionId),
Times.Once);
_mockBlobCodeFileRepository.Verify(x => x.GetCodeFileFromStorageAsync(It.IsAny<string>(), It.IsAny<string>()));
}

[Fact]
public async Task GetAPIRevisionTextAsync_WithOnlyReviewId_ReturnsBadRequest()
{
string reviewId = "review123";
ActionResult<string> result = await _controller.GetAPIRevisionTextAsync(null, reviewId);
ActionResult<string> result = await _controller.GetAPIRevisionContentAsync(null, reviewId);

BadRequestObjectResult badRequestResult = Assert.IsType<BadRequestObjectResult>(result.Result);
Assert.Equal("apiRevisionId is required", badRequestResult.Value);
Expand All @@ -100,10 +125,10 @@ public async Task GetAPIRevisionTextAsync_WithDeletedRevision_ReturnsNoContent()
.Setup(x => x.GetAPIRevisionAsync(It.IsAny<ClaimsPrincipal>(), apiRevisionId))
.ReturnsAsync(deletedRevision);

ActionResult<string> result = await _controller.GetAPIRevisionTextAsync(apiRevisionId);
ActionResult<string> result = await _controller.GetAPIRevisionContentAsync(apiRevisionId);

LeanJsonResult actionResult = Assert.IsType<LeanJsonResult>(result.Result);
Assert.Null(actionResult.Value);
NotFoundObjectResult notFoundResult = Assert.IsType<NotFoundObjectResult>(result.Result);
Assert.Equal("No API revision found for selection type: Undefined", notFoundResult.Value);
}

[Fact]
Expand All @@ -121,7 +146,7 @@ public async Task GetAPIRevisionTextAsync_LatestType_ReturnsLatestRevision()
.Setup(x => x.GetApiRevisionText(expectedRevision))
.ReturnsAsync(expectedText);

ActionResult<string> result = await _controller.GetAPIRevisionTextAsync(
ActionResult<string> result = await _controller.GetAPIRevisionContentAsync(
null,
reviewId,
APIRevisionSelectionType.Latest);
Expand Down Expand Up @@ -156,7 +181,7 @@ public async Task GetAPIRevisionTextAsync_LatestApprovedType_ReturnsLatestApprov
.Setup(x => x.GetApiRevisionText(expectedRevision))
.ReturnsAsync(expectedText);

ActionResult<string> result = await _controller.GetAPIRevisionTextAsync(
ActionResult<string> result = await _controller.GetAPIRevisionContentAsync(
null,
reviewId,
APIRevisionSelectionType.LatestApproved);
Expand All @@ -181,7 +206,7 @@ public async Task GetAPIRevisionTextAsync_LatestApprovedType_NoApprovedRevisions
.Setup(x => x.GetAPIRevisionsAsync(reviewId, "", APIRevisionType.All))
.ReturnsAsync(allRevisions);

ActionResult<string> result = await _controller.GetAPIRevisionTextAsync(null, reviewId, APIRevisionSelectionType.LatestApproved);
ActionResult<string> result = await _controller.GetAPIRevisionContentAsync(null, reviewId, APIRevisionSelectionType.LatestApproved);

NotFoundObjectResult notFoundResult = Assert.IsType<NotFoundObjectResult>(result.Result);
Assert.Equal("No API revision found for selection type: LatestApproved", notFoundResult.Value);
Expand All @@ -202,7 +227,7 @@ public async Task GetAPIRevisionTextAsync_LatestManualType_ReturnsLatestManualRe
.Setup(x => x.GetApiRevisionText(expectedRevision))
.ReturnsAsync(expectedText);

ActionResult<string> result = await _controller.GetAPIRevisionTextAsync(
ActionResult<string> result = await _controller.GetAPIRevisionContentAsync(
null,
reviewId,
APIRevisionSelectionType.LatestManual);
Expand All @@ -222,7 +247,7 @@ public async Task GetAPIRevisionTextAsync_LatestType_NoRevisionFound_ReturnsNotF
.Setup(x => x.GetLatestAPIRevisionsAsync(reviewId, null, APIRevisionType.All))
.ReturnsAsync((APIRevisionListItemModel)null);

ActionResult<string> result = await _controller.GetAPIRevisionTextAsync(
ActionResult<string> result = await _controller.GetAPIRevisionContentAsync(
null,
reviewId,
APIRevisionSelectionType.Latest);
Expand All @@ -240,7 +265,7 @@ public async Task GetAPIRevisionTextAsync_ExceptionThrown_ReturnsInternalServerE
.Setup(x => x.GetAPIRevisionAsync(It.IsAny<ClaimsPrincipal>(), apiRevisionId))
.ThrowsAsync(new Exception("Database error"));

ActionResult<string> result = await _controller.GetAPIRevisionTextAsync(
ActionResult<string> result = await _controller.GetAPIRevisionContentAsync(
apiRevisionId);

ObjectResult statusCodeResult = Assert.IsType<ObjectResult>(result.Result);
Expand All @@ -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 = [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,14 +21,16 @@ namespace APIViewWeb.LeanControllers;
public class APIRevisionsTokenAuthController : ControllerBase
{
private readonly IAPIRevisionsManager _apiRevisionsManager;

private readonly IBlobCodeFileRepository _codeFileRepository;
private readonly ILogger<APIRevisionsTokenAuthController> _logger;

public APIRevisionsTokenAuthController(ILogger<APIRevisionsTokenAuthController> logger,
IAPIRevisionsManager apiRevisionsManager)
public APIRevisionsTokenAuthController(IBlobCodeFileRepository codeFileRepository,
IAPIRevisionsManager apiRevisionsManager,
ILogger<APIRevisionsTokenAuthController> logger)
{
_logger = logger;
_apiRevisionsManager = apiRevisionsManager;
_codeFileRepository = codeFileRepository;
_logger = logger;
}

/// <summary>
Expand Down Expand Up @@ -55,12 +59,14 @@ public async Task<ActionResult<APIRevisionListItemModel>> GetOutlineAPIRevisionA
/// <param name="reviewId">The review ID</param>
/// <param name="apiRevisionId">The specific API revision ID (required when there is not selectionType)</param>
/// <param name="selectionType">How to select the API revision</param>
/// <param name="contentReturnType">The content return type. Default is text, but CodeFile can also be selected</param>
/// <returns>Plain text representation of the API review</returns>
[HttpGet("getRevisionText", Name = "GetAPIRevisionText")]
public async Task<ActionResult<string>> GetAPIRevisionTextAsync(
[HttpGet("getRevisionContent", Name = "GetAPIRevisionContent")]
public async Task<ActionResult> 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
{
Expand All @@ -74,57 +80,35 @@ public async Task<ActionResult<string>> 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<APIRevisionListItemModel> 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)
{
Expand All @@ -134,4 +118,38 @@ public async Task<ActionResult<string>> GetAPIRevisionTextAsync(
return StatusCode(StatusCodes.Status500InternalServerError, "Failed to generate review text");
}
}

private async Task<APIRevisionListItemModel> 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<APIRevisionListItemModel> GetLatestApprovedRevision(string reviewId)
{
IEnumerable<APIRevisionListItemModel> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,10 @@ public enum APIRevisionSelectionType
/// Use the latest manual revision (non-automatic)
/// </summary>
LatestManual = 4
}

public enum APIRevisionContentReturnType
{
Text,
CodeFile
}