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
21 changes: 4 additions & 17 deletions Apps/Find/src/SUI.Find.FindApi/Startup/AzureStorageQueueStartup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,15 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using SUI.Find.Application.Constants;
using SUI.Find.Infrastructure.Factories;

namespace SUI.Find.FindApi.Startup;

[ExcludeFromCodeCoverage(Justification = "Hosted service startup code.")]
public class AzureStorageQueueStartup : IHostedService
public class AzureStorageQueueStartup(IQueueClientFactory queueClientFactory) : IHostedService
{
private readonly IConfiguration _configuration;

public AzureStorageQueueStartup(IConfiguration configuration)
{
_configuration = configuration;
}

public async Task StartAsync(CancellationToken cancellationToken)
{
var connectionString = _configuration["AzureWebJobsStorage"];
var queueClient = new QueueClient(
connectionString,
ApplicationConstants.SearchJobs.QueueName
);
await queueClient.CreateIfNotExistsAsync(cancellationToken: cancellationToken);
}
public async Task StartAsync(CancellationToken cancellationToken) =>
await queueClientFactory.CreateQueuesIfNotExistsAsync(cancellationToken);

public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using SUI.Find.Infrastructure.Configuration;
using SUI.Find.Infrastructure.Factories;
using SUI.Find.Infrastructure.Factories.Fhir;
using SUI.Find.Infrastructure.Handlers;
using SUI.Find.Infrastructure.Interfaces;
using SUI.Find.Infrastructure.Interfaces.Fhir;
using SUI.Find.Infrastructure.Repositories.JobRepository;
Expand Down Expand Up @@ -67,6 +68,7 @@ IConfiguration configuration
services.AddSingleton<IJobClaimService, JobClaimService>();
services.AddSingleton<IJobProcessorService, JobProcessorService>();
services.AddSingleton<IJobResultsQueueClient, JobResultsQueueClient>();
services.AddSingleton<IJobResultHandler, JobResultHandler>();

return services;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,29 @@ namespace SUI.Find.Infrastructure.Factories;

public interface IQueueClientFactory
{
/// <summary>
/// Creates the required queues. If any queue already exists, it is not changed.
/// </summary>
/// <param name="cancellationToken">Optional token to propagate notifications that the operation should be cancelled.</param>
/// <returns>Async operation task</returns>
Task CreateQueuesIfNotExistsAsync(CancellationToken cancellationToken);

/// <summary>
/// Creates a client for sending messages to the Audit queue.
/// </summary>
/// <returns>Audit queue client</returns>
IAuditQueueSender GetAuditClient();

/// <summary>
/// Creates a client for sending messages to the Search Job queue.
/// </summary>
/// <returns>Search Job queue client</returns>
ISearchJobQueueSender GetSearchJobClient();

/// <summary>
/// Creates a client for sending messages to the Job Results queue.
/// </summary>
/// <returns>Job Results queue client</returns>
IJobResultsQueueSender GetJobResultsClient();
}

Expand All @@ -22,30 +43,35 @@ public class QueueClientFactory(IConfiguration config) : IQueueClientFactory
private readonly string _azureWebJobsStorageConnectionString =
config["AzureWebJobsStorage"] ?? throw new InvalidOperationException();

public IAuditQueueSender GetAuditClient()
/// <inheritdoc/>
public async Task CreateQueuesIfNotExistsAsync(CancellationToken cancellationToken)
{
return new AzureQueueSender(
new QueueClient(_auditConnectionString, ApplicationConstants.Audit.AccessQueueName)
);
}
await CreateAuditQueueClient().CreateIfNotExistsAsync(cancellationToken: cancellationToken);

public ISearchJobQueueSender GetSearchJobClient()
{
return new AzureSearchJobQueueSender(
new QueueClient(
_azureWebJobsStorageConnectionString,
ApplicationConstants.SearchJobs.QueueName
)
);
}
await CreateSearchJobQueueClient()
.CreateIfNotExistsAsync(cancellationToken: cancellationToken);

public IJobResultsQueueSender GetJobResultsClient()
{
return new AzureQueueSender(
new QueueClient(
_azureWebJobsStorageConnectionString,
ApplicationConstants.Jobs.JobResultsQueueName
)
);
await CreateJobResultsQueueClient()
.CreateIfNotExistsAsync(cancellationToken: cancellationToken);
}

/// <inheritdoc/>
public IAuditQueueSender GetAuditClient() => new AzureQueueSender(CreateAuditQueueClient());

/// <inheritdoc/>
public ISearchJobQueueSender GetSearchJobClient() =>
new AzureSearchJobQueueSender(CreateSearchJobQueueClient());

/// <inheritdoc/>
public IJobResultsQueueSender GetJobResultsClient() =>
new AzureQueueSender(CreateJobResultsQueueClient());

private QueueClient CreateAuditQueueClient() =>
new(_auditConnectionString, ApplicationConstants.Audit.AccessQueueName);

private QueueClient CreateSearchJobQueueClient() =>
new(_azureWebJobsStorageConnectionString, ApplicationConstants.SearchJobs.QueueName);

private QueueClient CreateJobResultsQueueClient() =>
new(_azureWebJobsStorageConnectionString, ApplicationConstants.Jobs.JobResultsQueueName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ await workItemJobCountRepository.GetByWorkItemIdAndJobTypeAsync(

if (workItemJobCountEntity == null || workItemJobCountEntity.ExpectedJobCount == 0)
{
logger.LogInformation("No jobs found for work item ID {WorkItemId}", workItemId);
if (logger.IsEnabled(LogLevel.Information))
logger.LogInformation("No jobs found for work item ID {WorkItemId}", workItemId);
return new NotFound();
}

Expand All @@ -55,8 +56,10 @@ await workItemJobCountRepository.GetByWorkItemIdAndJobTypeAsync(
cancellationToken
);

var completedJobCount = completedRecords.DistinctBy(record => record.JobId).Count();

var completenessPercentage =
completedRecords.Count * 100 / workItemJobCountEntity.ExpectedJobCount;
completedJobCount * 100 / workItemJobCountEntity.ExpectedJobCount;

var status = GetOverallJobStatus(completenessPercentage, workItemJobCountEntity);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.Extensions.Logging;
using NSubstitute;
using OneOf.Types;
using Shouldly;
using SUI.Find.Application.Dtos;
using SUI.Find.Application.Enums;
using SUI.Find.Application.Interfaces;
Expand All @@ -27,6 +28,7 @@ public class JobSearchServiceTests

public JobSearchServiceTests()
{
_logger.IsEnabled(LogLevel.Information).Returns(true);
_jobWindowStartService.GetWindowStart().Returns(_dateTime.AddHours(-72));
_sut = new JobSearchService(
_searchResultEntryRepository,
Expand Down Expand Up @@ -211,6 +213,90 @@ public async Task GetSearchResults_CorrectlyReturnsCompletedResults()
Assert.Equal(searchResults, results.AsT0.Items);
}

[Fact]
public async Task GetSearchResults_UsesDistinctJobCount_ToDeriveCompletenessPercentage()
{
var workItemId = $"wi-{Guid.NewGuid()}";
var searchingOrganisationId = $"so-{Guid.NewGuid()}";

var payload = new CustodianLookupJobPayload("SUI-1", "Health");

var workItemJobCount = new WorkItemJobCount
{
WorkItemId = workItemId,
JobType = JobType.CustodianLookup,
SearchingOrganisationId = searchingOrganisationId,
PayloadJson = JsonSerializer.Serialize(payload, JsonSerializerOptions.Web),
ExpectedJobCount = 2,
CreatedAtUtc = _dateTime.AddHours(-6),
UpdatedAtUtc = _dateTime.AddHours(-2),
};

_workItemJobCountRepository
.GetByWorkItemIdAndJobTypeAsync(
workItemId,
JobType.CustodianLookup,
Arg.Any<CancellationToken>()
)
.Returns(workItemJobCount);

IReadOnlyList<SearchResultEntry> searchResults =
[
new()
{
CustodianId = "CUS-1",
CustodianName = "Custodian-1",
JobId = "JOB-1",
WorkItemId = workItemId,
RecordType = "HEALTH",
SystemId = "SYS-1",
RecordUrl = "URL-1",
RecordId = "12345",
SearchingOrganisationId = searchingOrganisationId,
},
new()
{
CustodianId = "CUS-1",
CustodianName = "Custodian-1",
JobId = "JOB-1",
WorkItemId = workItemId,
RecordType = "HEALTH",
SystemId = "SYS-2",
RecordUrl = "URL-2",
RecordId = "xyz",
SearchingOrganisationId = searchingOrganisationId,
},
new()
{
CustodianId = "CUS-1",
CustodianName = "Custodian-1",
JobId = "JOB-1",
WorkItemId = workItemId,
RecordType = "HEALTH",
SystemId = "SYS-3",
RecordUrl = "URL-3",
RecordId = "efg",
SearchingOrganisationId = searchingOrganisationId,
},
];

_searchResultEntryRepository
.GetByWorkItemIdAsync(workItemId, searchingOrganisationId, Arg.Any<CancellationToken>())
.Returns(searchResults);

var results = await _sut.GetSearchResultsAsync(
workItemId,
searchingOrganisationId,
CancellationToken.None
);

var resultsDto = Assert.IsType<SearchResultsV2Dto>(results.Value);
resultsDto.CompletenessPercentage.ShouldBe(50);
resultsDto.Status.ShouldBe(SearchStatus.Running);
resultsDto.WorkItemId.ShouldBe(workItemId);
resultsDto.Suid.ShouldBe("SUI-1");
}

[Fact]
public async Task GetSearchResults_CorrectlyReturnsExpiredResults()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ public FindApiClient(HttpClient http)

public async Task<JobInfo?> ClaimAsync(string token)
{
var req = new HttpRequestMessage(HttpMethod.Post, "/v2/work/claim");
using var req = new HttpRequestMessage(HttpMethod.Post, "/api/v2/work/claim");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);

var res = await _http.SendAsync(req);
using var res = await _http.SendAsync(req);

if (res.StatusCode == HttpStatusCode.NoContent)
{
Expand All @@ -37,11 +37,15 @@ public FindApiClient(HttpClient http)

public async Task SubmitAsync(string token, SubmitJobResultsRequest request)
{
var req = new HttpRequestMessage(HttpMethod.Post, "/v2/work/result");
using var req = new HttpRequestMessage(HttpMethod.Post, "/api/v2/work/result");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
req.Content = JsonContent.Create(request);

var res = await _http.SendAsync(req);
using var content = JsonContent.Create(request);
await content.LoadIntoBufferAsync(); // Local only concern, stops the HTTP client using chunked transfer encoding, which is not supported by the receiving end (Azure Functions Core Tools local dev host)

req.Content = content;

using var res = await _http.SendAsync(req);

if (res.StatusCode == HttpStatusCode.Conflict)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public async Task ClaimAsync_ShouldReturnJob_WhenJobAvailable()

var handler = new FakeHandler(req =>
{
Assert.Equal("/v2/work/claim", req.RequestUri!.AbsolutePath);
Assert.Equal("/api/v2/work/claim", req.RequestUri!.AbsolutePath);
Assert.Equal("Bearer", req.Headers.Authorization!.Scheme);
Assert.Equal("token", req.Headers.Authorization.Parameter);

Expand Down Expand Up @@ -82,7 +82,7 @@ public async Task SubmitAsync_ShouldPostResults()
{
var handler = new FakeHandler(req =>
{
Assert.Equal("/v2/work/result", req.RequestUri!.AbsolutePath);
Assert.Equal("/api/v2/work/result", req.RequestUri!.AbsolutePath);
Assert.Equal("Bearer", req.Headers.Authorization!.Scheme);
Assert.Equal("token", req.Headers.Authorization.Parameter);

Expand Down
Loading