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
10 changes: 7 additions & 3 deletions src/DfE.CheckPerformanceData.Infrastructure/DependencyManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,13 @@ public static IServiceCollection AddInfrastructureDependencies(this IServiceColl
// by every host that calls AddPersistenceDependencies — including the worker.
services.AddScoped<IPupilDataBlobClient, PupilDataBlobClient>();

// AB#296648: the 16-19 exam results a school can raise an enquiry against, held in the same
// per-window container under the results-enquiry checking-exercise prefix.
services.AddScoped<IStudentResultsClient, StudentResultsBlobClient>();
// IStudentResultsClient is deliberately NOT registered here. Its implementation takes an
// IMemoryCache, which this bundle's only caller — the worker — does not have: AddMemoryCache
// comes from AddPersistenceDependencies, and the worker opts out of that so its manual
// DbContext registration stays the single source of truth. Registering it here therefore
// failed validate-on-build and took the whole worker process down, consumers and retention
// jobs included, over a service the worker never resolves. Every consumer is in the web
// host, which assembles its own blob clients in AddCpdBlobStorage and registers it there.

// Analytics sink: the real dfe-analytics adapter when DfeAnalytics:DatasetId is
// configured (deployed envs wire it via Terraform), else a no-op so dev/review/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ public sealed class TurnaroundCommitmentTests(PlaywrightFixture fixture) : Seedi
// The seeded KS4 June window (see SeedCheckingWindows in DevDataSeeder).
private static readonly Guid SeededWindowId = Guid.Parse("F34D285B-8660-4D12-9C30-787328DEAA0A");

// What the Summary prints in place of an unset commitment.
private const string NotSetPlaceholder = "Not set";

private string SummaryUrl => $"{Fixture.BaseUrl}/admin/windows/summary/{SeededWindowId}";
private string EditUrl => $"{Fixture.BaseUrl}/admin/windows/{SeededWindowId}/turnaround-commitment";

Expand All @@ -40,9 +43,16 @@ public async Task EditPage_PrefillsCurrentValue()
await Page.GotoAsync(SummaryUrl);
var value = await CurrentSummaryValueAsync();

// "Not set" is what the Summary prints when there is no value — a placeholder, not the
// value itself, so the edit field is legitimately empty in that state. Comparing the two
// directly made this test depend on running before EmptySubmission_IsAllowed_AndShowsNotSet,
// which clears the commitment. xUnit does not order tests within a class, so whichever
// order a run happened to pick decided whether this passed.
var expected = value == NotSetPlaceholder ? string.Empty : value;

await Page.GotoAsync(EditUrl);
var input = Page.Locator("#TurnaroundCommitment");
await Expect(input).ToHaveValueAsync(value);
await Expect(input).ToHaveValueAsync(expected);
}

[Fact]
Expand All @@ -68,7 +78,7 @@ public async Task EmptySubmission_IsAllowed_AndShowsNotSet()
await Page.WaitForURLAsync("**/admin/windows/summary/**");

await Expect(Page.Locator(".govuk-error-summary")).ToHaveCountAsync(0);
Assert.Equal("Not set", await CurrentSummaryValueAsync());
Assert.Equal(NotSetPlaceholder, await CurrentSummaryValueAsync());
}

private async Task<string> CurrentSummaryValueAsync()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using DfE.CheckPerformanceData.Infrastructure;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace DfE.CheckPerformanceData.Application.UnitTests.Infrastructure;

// Guards the registration bundle the rules-engine worker builds its container from.
//
// The worker is the only host that calls AddInfrastructureDependencies. The web host does not —
// it assembles its own set in AddCpdBlobStorage — so a service added to this bundle for the web's
// benefit is executed only by the worker. If its dependencies are not also in the worker's
// container, validate-on-build kills the host: the queue consumer, the dead-letter, metrics,
// search-analytics and content-staging retention jobs all stop, over a service none of them uses.
//
// That has now happened twice. The Build workflow cannot catch it, because it compiles and runs
// tests and never starts the worker, so the failure reaches whoever runs the container next.
public class InfrastructureDependenciesTests
{
// Azurite's well-known development account. Nothing connects during registration or
// validation, so no storage emulator has to be running for this test.
private const string AzuriteConnection =
"DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;" +
"AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;" +
"BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;";

// Mirrors the registrations RulesEngineWorker/Program.cs makes before and after its call to
// AddInfrastructureDependencies — the collaborators the bundle is entitled to assume. Keep in
// step with that file: anything the worker stops registering has to come out of here too, or
// this test vouches for a container the worker does not actually have.
private static IServiceCollection WorkerServices()
{
var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
{
// The blob clients in this bundle take a BlobServiceClient, which the bundle only
// registers when a storage connection string is present. Compose and every deployed
// environment supply one, so a test without it would be checking a container shape
// no host ever has.
["ConnectionStrings:AzureStorage"] = AzuriteConnection,
["ZendeskSettings:Subdomain"] = "dfe",
["ZendeskSettings:Domain"] = "zendesk",
["ZendeskSettings:Email"] = "cypmd@education.gov.uk",
["ZendeskSettings:ApiToken"] = "token",
}).Build();

var services = new ServiceCollection();
services.AddLogging();
services.AddInfrastructureDependencies(config);
return services;
}

// Constructing every registration is exactly what the host does at startup, and exactly what
// the worker died doing. Asserting it here turns a crash-loop into a red test.
[Fact]
public void EveryRegistrationInTheBundle_CanBeConstructed()
{
var services = WorkerServices();

var exception = Record.Exception(() => services.BuildServiceProvider(
new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true }));

Assert.Null(exception);
}
}
Loading