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 @@ -12,6 +12,7 @@
using CPS.ComplexCases.Common.Helpers;
using CPS.ComplexCases.Common.Models;
using CPS.ComplexCases.Common.Services;
using CPS.ComplexCases.Data.Entities;
using CPS.ComplexCases.Data.Models.Requests;
using CPS.ComplexCases.NetApp.Client;
using CPS.ComplexCases.NetApp.Factories;
Expand Down Expand Up @@ -171,6 +172,10 @@ public async Task Run_ValidRequestWithPermission_CreatesConnectionAndReturnsOk()
.Setup(x => x.ListFoldersInBucketAsync(netAppArg))
.ReturnsAsync(netAppResponse);

_caseMetadataServiceMock
.Setup(x => x.GetCaseMetadataForNetAppFolderPathsAsync(It.IsAny<IEnumerable<string>>()))
.ReturnsAsync(Enumerable.Empty<CaseMetadata>());

_caseMetadataServiceMock
.Setup(x => x.CreateNetAppConnectionAsync(netAppConnectionRequest))
.Returns(Task.CompletedTask);
Expand Down Expand Up @@ -204,6 +209,133 @@ public async Task Run_ValidRequestWithPermission_CreatesConnectionAndReturnsOk()
_testUsername, null), Times.Once);
}

[Fact]
public async Task Run_WhenFolderPathAlreadyConnected_ReturnsConflict()
{
// Arrange
var netAppConnectionRequest = _fixture.Create<CreateNetAppConnectionDto>();
var netAppArg = _fixture.Create<ListFoldersInBucketArg>();
var netAppResponse = _fixture.Create<ListNetAppObjectsDto>();
var existingCaseId = _fixture.Create<int>();
var folderPath = netAppConnectionRequest.NetAppFolderPath;
var legacyPath = $"{_testBucketName}:{folderPath}";

_requestValidatorMock
.Setup(x => x.GetJsonBody<CreateNetAppConnectionDto, CreateNetAppConnectionValidator>(It.IsAny<HttpRequest>()))
.ReturnsAsync(new ValidatableRequest<CreateNetAppConnectionDto>
{
IsValid = true,
Value = netAppConnectionRequest
});

_netAppArgFactoryMock
.Setup(x => x.CreateListFoldersInBucketArg(_testBearerToken, _testBucketName, netAppConnectionRequest.OperationName, null, 1, null))
.Returns(netAppArg);

_netAppClientMock
.Setup(x => x.ListFoldersInBucketAsync(netAppArg))
.ReturnsAsync(netAppResponse);

_caseMetadataServiceMock
.Setup(x => x.GetCaseMetadataForNetAppFolderPathsAsync(It.Is<IEnumerable<string>>(paths =>
paths.Contains(folderPath) && paths.Contains(legacyPath))))
.ReturnsAsync([
new CaseMetadata
{
CaseId = existingCaseId,
NetappFolderPath = folderPath
}
]);

var request = HttpRequestStubHelper.CreateHttpRequestFor(netAppConnectionRequest);
var functionContext = FunctionContextStubHelper.CreateFunctionContextStub(_testCorrelationId, _testCmsAuthValues, _testUsername, _testBearerToken);

// Act
var result = await _function.Run(request, functionContext);

// Assert
var conflict = Assert.IsType<ConflictObjectResult>(result);
Assert.Equal(
$"Folder path '{folderPath}' is already connected to another case.",
conflict.Value);

_caseMetadataServiceMock.Verify(x => x.GetCaseMetadataForNetAppFolderPathsAsync(It.Is<IEnumerable<string>>(paths =>
paths.Contains(folderPath) && paths.Contains(legacyPath))), Times.Once);
_caseMetadataServiceMock.Verify(x => x.CreateNetAppConnectionAsync(It.IsAny<CreateNetAppConnectionDto>()), Times.Never);
_activityLogServiceMock.Verify(x => x.CreateActivityLogAsync(
It.IsAny<ActivityLog.Enums.ActionType>(),
It.IsAny<ActivityLog.Enums.ResourceType>(),
It.IsAny<int>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
null), Times.Never);
_loggerMock.Verify(
x => x.Log(
LogLevel.Warning,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) =>
v.ToString()!.Contains(folderPath) &&
v.ToString()!.Contains(existingCaseId.ToString()) &&
v.ToString()!.Contains(netAppConnectionRequest.CaseId.ToString())),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
}

[Fact]
public async Task Run_WhenLegacyBucketPathAlreadyConnected_ReturnsConflict()
{
// Arrange
var netAppConnectionRequest = _fixture.Create<CreateNetAppConnectionDto>();
var netAppArg = _fixture.Create<ListFoldersInBucketArg>();
var netAppResponse = _fixture.Create<ListNetAppObjectsDto>();
var existingCaseId = _fixture.Create<int>();
var folderPath = netAppConnectionRequest.NetAppFolderPath;
var legacyPath = $"{_testBucketName}:{folderPath}";

_requestValidatorMock
.Setup(x => x.GetJsonBody<CreateNetAppConnectionDto, CreateNetAppConnectionValidator>(It.IsAny<HttpRequest>()))
.ReturnsAsync(new ValidatableRequest<CreateNetAppConnectionDto>
{
IsValid = true,
Value = netAppConnectionRequest
});

_netAppArgFactoryMock
.Setup(x => x.CreateListFoldersInBucketArg(_testBearerToken, _testBucketName, netAppConnectionRequest.OperationName, null, 1, null))
.Returns(netAppArg);

_netAppClientMock
.Setup(x => x.ListFoldersInBucketAsync(netAppArg))
.ReturnsAsync(netAppResponse);

_caseMetadataServiceMock
.Setup(x => x.GetCaseMetadataForNetAppFolderPathsAsync(It.Is<IEnumerable<string>>(paths =>
paths.Contains(folderPath) && paths.Contains(legacyPath))))
.ReturnsAsync([
new CaseMetadata
{
CaseId = existingCaseId,
NetappFolderPath = legacyPath
}
]);

var request = HttpRequestStubHelper.CreateHttpRequestFor(netAppConnectionRequest);
var functionContext = FunctionContextStubHelper.CreateFunctionContextStub(_testCorrelationId, _testCmsAuthValues, _testUsername, _testBearerToken);

// Act
var result = await _function.Run(request, functionContext);

// Assert
var conflict = Assert.IsType<ConflictObjectResult>(result);
Assert.Equal(
$"Folder path '{folderPath}' is already connected to another case.",
conflict.Value);

_caseMetadataServiceMock.Verify(x => x.CreateNetAppConnectionAsync(It.IsAny<CreateNetAppConnectionDto>()), Times.Never);
}

[Fact]
public async Task Run_NetAppArgFactoryCalledWithCorrectParameters()
{
Expand Down Expand Up @@ -340,6 +472,10 @@ public async Task Run_WhenActivityLogThrows_StillReturnsOk()
.Setup(x => x.ListFoldersInBucketAsync(netAppArg))
.ReturnsAsync(netAppResponse);

_caseMetadataServiceMock
.Setup(x => x.GetCaseMetadataForNetAppFolderPathsAsync(It.IsAny<IEnumerable<string>>()))
.ReturnsAsync(Enumerable.Empty<CaseMetadata>());

_caseMetadataServiceMock
.Setup(x => x.CreateNetAppConnectionAsync(netAppConnectionRequest))
.Returns(Task.CompletedTask);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ public async Task EnrichNetAppFoldersWithMetadataAsync_WhenFoldersProvided_Enric
var folders = CreateSampleNetAppFolders(3);
var folderPaths = folders.Data.FolderData
.Where(d => d.Path != null)
.Select(d => $"{folders.Data.BucketName}:{d.Path}")
.Select(d => d.Path!)
.ToList();

var metadata = folderPaths.Select(path => new CaseMetadata
Expand All @@ -430,7 +430,7 @@ public async Task EnrichNetAppFoldersWithMetadataAsync_WhenFoldersProvided_Enric

_caseMetadataServiceMock
.Setup(s => s.GetCaseMetadataForNetAppFolderPathsAsync(It.Is<IEnumerable<string>>(paths =>
paths.All(p => folderPaths.Contains(p)) && paths.Count() == folderPaths.Count)))
folderPaths.All(p => paths.Contains(p)))))
.ReturnsAsync(metadata);

// Act
Expand All @@ -441,8 +441,7 @@ public async Task EnrichNetAppFoldersWithMetadataAsync_WhenFoldersProvided_Enric

foreach (var folderResponse in result.Data.Folders)
{
var expectedPath = $"{folders.Data.BucketName}:{folderResponse.FolderPath}";
var expectedMetadata = metadata.First(m => m.NetappFolderPath == expectedPath);
var expectedMetadata = metadata.First(m => m.NetappFolderPath == folderResponse.FolderPath);
Assert.Equal(expectedMetadata.CaseId, folderResponse.CaseId);
}

Expand All @@ -451,7 +450,7 @@ public async Task EnrichNetAppFoldersWithMetadataAsync_WhenFoldersProvided_Enric

_caseMetadataServiceMock.Verify(
s => s.GetCaseMetadataForNetAppFolderPathsAsync(It.Is<IEnumerable<string>>(paths =>
paths.All(p => folderPaths.Contains(p)) && paths.Count() == folderPaths.Count)),
folderPaths.All(p => paths.Contains(p)))),
Times.Once);
}

Expand All @@ -462,7 +461,7 @@ public async Task EnrichNetAppFoldersWithMetadataAsync_WhenMetadataNotFoundForSo
var folders = CreateSampleNetAppFolders(3);
var folderPaths = folders.Data.FolderData
.Where(d => d.Path != null)
.Select(d => $"{folders.Data.BucketName}:{d.Path}")
.Select(d => d.Path!)
.ToList();

var metadata = new List<CaseMetadata>
Expand Down Expand Up @@ -494,6 +493,35 @@ public async Task EnrichNetAppFoldersWithMetadataAsync_WhenMetadataNotFoundForSo
}
}

[Fact]
public async Task EnrichNetAppFoldersWithMetadataAsync_WhenMetadataStoredAsBucketPath_StillEnriches()
{
// Arrange
var folders = CreateSampleNetAppFolders(1);
var folderPath = folders.Data.FolderData.First().Path!;
var legacyPath = $"{folders.Data.BucketName}:{folderPath}";
var metadata = new List<CaseMetadata>
{
new CaseMetadata
{
CaseId = _fixture.Create<int>(),
NetappFolderPath = legacyPath
}
};

_caseMetadataServiceMock
.Setup(s => s.GetCaseMetadataForNetAppFolderPathsAsync(It.Is<IEnumerable<string>>(paths =>
paths.Contains(folderPath) && paths.Contains(legacyPath))))
.ReturnsAsync(metadata);

// Act
var result = await _service.EnrichNetAppFoldersWithMetadataAsync(folders);

// Assert
Assert.Equal(metadata.First().CaseId, result.Data.Folders.Single().CaseId);
Assert.Equal(folderPath, result.Data.Folders.Single().FolderPath);
}

[Fact]
public async Task EnrichNetAppFoldersWithMetadataAsync_WhenNoFoldersProvided_ReturnsEmptyCollection()
{
Expand Down Expand Up @@ -608,7 +636,7 @@ public async Task EnrichNetAppFoldersWithMetadataAsync_WhenMetadataHasNullNetapp
var folders = CreateSampleNetAppFolders(2);
var folderPaths = folders.Data.FolderData
.Where(d => d.Path != null)
.Select(d => $"{folders.Data.BucketName}:{d.Path}")
.Select(d => d.Path!)
.ToList();

var metadata = new List<CaseMetadata>
Expand Down
15 changes: 15 additions & 0 deletions backend/CPS.ComplexCases.API/Functions/CreateNetAppConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
[OpenApiResponseWithBody(statusCode: HttpStatusCode.BadRequest, contentType: ContentType.TextPlain, typeof(string), Description = ApiResponseDescriptions.BadRequest)]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.Unauthorized, contentType: ContentType.TextPlain, typeof(string), Description = ApiResponseDescriptions.Unauthorized)]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.Forbidden, contentType: ContentType.TextPlain, typeof(string), Description = ApiResponseDescriptions.Forbidden)]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.Conflict, contentType: ContentType.TextPlain, typeof(string), Description = ApiResponseDescriptions.Conflict)]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.InternalServerError, contentType: ContentType.TextPlain, typeof(string), Description = ApiResponseDescriptions.InternalServerError)]
public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "v1/netapp/connections")] HttpRequest req, FunctionContext functionContext)
{
Expand All @@ -71,6 +72,20 @@
return new UnauthorizedResult();
}

var folderPath = netAppConnectionRequest.Value.NetAppFolderPath;
var bucketName = securityGroups.First().BucketName;

Check warning on line 76 in backend/CPS.ComplexCases.API/Functions/CreateNetAppConnection.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Indexing at 0 should be used instead of the "Enumerable" extension method "First"

See more on https://sonarcloud.io/project/issues?id=CPS-Innovation_Large-and-Complex-Cases&issues=AZ-3McX6heGJ2RZw5T56&open=AZ-3McX6heGJ2RZw5T56&pullRequest=528
var lookupPaths = new[] { folderPath, $"{bucketName}:{folderPath}" };
var existingConnections = await _caseMetadataService.GetCaseMetadataForNetAppFolderPathsAsync(lookupPaths);
var existingConnection = existingConnections.FirstOrDefault();
Comment thread
bridgesr marked this conversation as resolved.

if (existingConnection != null)
{
_logger.LogWarning(
"Duplicate NetApp connection attempt: folder path '{FolderPath}' is already connected to case {ExistingCaseId}. Requested by case {RequestedCaseId}.",
folderPath, existingConnection.CaseId, netAppConnectionRequest.Value.CaseId);
return new ConflictObjectResult($"Folder path '{folderPath}' is already connected to another case.");
}
Comment thread
bridgesr marked this conversation as resolved.

await _caseMetadataService.CreateNetAppConnectionAsync(netAppConnectionRequest.Value);

try
Expand Down
44 changes: 35 additions & 9 deletions backend/CPS.ComplexCases.API/Services/CaseEnrichmentService.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
using Microsoft.Extensions.Logging;
using CPS.ComplexCases.API.Domain.Response;
using CPS.ComplexCases.Common.Services;
using CPS.ComplexCases.Data.Entities;
using CPS.ComplexCases.DDEI.Models.Dto;
using CPS.ComplexCases.Egress.Models.Dto;
using CPS.ComplexCases.NetApp.Models.Dto;
using CPS.ComplexCases.Common.Services;

namespace CPS.ComplexCases.API.Services;

Expand Down Expand Up @@ -107,22 +108,29 @@

try
{
var folderPaths = folders.Data.FolderData.Where(d => d.Path != null)
.Select(d => $"{folders.Data.BucketName}:{d.Path}")
.ToList();

var metadata = await _caseMetadataService.GetCaseMetadataForNetAppFolderPathsAsync(folderPaths);
var folderPaths = folders.Data.FolderData
.Where(d => d.Path != null)
.Select(d => d.Path!)

Check warning on line 113 in backend/CPS.ComplexCases.API/Services/CaseEnrichmentService.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this null-forgiving operator; nullable warnings are disabled here.

See more on https://sonarcloud.io/project/issues?id=CPS-Innovation_Large-and-Complex-Cases&issues=AZ-zg4sosck-IeVUxipO&open=AZ-zg4sosck-IeVUxipO&pullRequest=528
.ToList();

// Look up plain paths (as stored on connect) and bucket:path variants for any legacy rows
var lookupPaths = folderPaths
.SelectMany(path => new[] { path, $"{folders.Data.BucketName}:{path}" })
.Distinct()
.ToList();

var metadata = await _caseMetadataService.GetCaseMetadataForNetAppFolderPathsAsync(lookupPaths);
var metadataLookup = metadata
.Where(m => m.NetappFolderPath != null)
.ToDictionary(m => m.NetappFolderPath!);

// Enrich data with metadata
response.Data = new ListNetAppObjectsDataResponse
{
Folders = folderPaths.Select(folder => new ListNetAppFoldersDataResponse
Folders = folderPaths.Select(folderPath => new ListNetAppFoldersDataResponse
{
FolderPath = folder[(folder.LastIndexOf(':') + 1)..] ?? string.Empty,
CaseId = metadataLookup.TryGetValue(folder, out var caseMetadata) ? caseMetadata.CaseId : null
FolderPath = folderPath,
CaseId = ResolveNetAppFolderCaseId(folderPath, folders.Data.BucketName, metadataLookup)
}).ToList(),
Files = new List<ListNetAppFilesDataResponse>(),
RootPath = folders.Data.RootPath
Expand All @@ -137,6 +145,24 @@
}
}

private static int? ResolveNetAppFolderCaseId(
string folderPath,
string bucketName,
IReadOnlyDictionary<string, CaseMetadata> metadataLookup)
{
if (metadataLookup.TryGetValue(folderPath, out var caseMetadata))
{
return caseMetadata.CaseId;
}

if (metadataLookup.TryGetValue($"{bucketName}:{folderPath}", out caseMetadata))
{
return caseMetadata.CaseId;
}

return null;
}

private static CaseWithMetadataResponse MapCaseToResponse(CaseDto caseDto)
{
return new CaseWithMetadataResponse
Expand Down
Loading