Skip to content

Commit 728c635

Browse files
committed
Merge branch 'dev' into users/jbritton/redisFixes
2 parents a28a2a0 + 1aac4ad commit 728c635

9 files changed

Lines changed: 389 additions & 7 deletions

File tree

DotNet/ServiceTests/IntegrationTests/Census/CensusIntegrationTestFixture.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,4 +190,6 @@ internal class NullTenantApiService : ITenantApiService
190190
public Task<bool> CheckFacilityExists(string facilityId, CancellationToken cancellationToken = default) => Task.FromResult(true);
191191
public Task<FacilityModel> GetFacilityConfig(string facilityId, CancellationToken cancellationToken = default)
192192
=> Task.FromResult(new FacilityModel { FacilityId = facilityId });
193+
public Task<string?> GetVendorSigningKeySecretId(string facilityId, CancellationToken cancellationToken = default)
194+
=> Task.FromResult<string?>(null);
193195
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
using LantanaGroup.Link.Shared.Application.Models.Tenant;
2+
using LantanaGroup.Link.Tenant.Business.Queries;
3+
using LantanaGroup.Link.Tenant.Data.Entities;
4+
using LantanaGroup.Link.Tenant.Entities;
5+
using LantanaGroup.Link.Tenant.Repository.Context;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Task = System.Threading.Tasks.Task;
8+
9+
namespace IntegrationTests.Tenant;
10+
11+
/// <summary>
12+
/// Data Acquisition reads the vendor's signing key off the facility response rather than making a
13+
/// second call for the vendor, so the facility projection has to carry it.
14+
/// </summary>
15+
[Collection("IntegrationTests")]
16+
[Trait("Category", "IntegrationTests")]
17+
public class FacilityVendorAuthenticationTests : IDisposable
18+
{
19+
private readonly IServiceScope _scope;
20+
private readonly IFacilityQueries _facilityQueries;
21+
private readonly TenantDbContext _dbContext;
22+
23+
public FacilityVendorAuthenticationTests(TenantIntegrationTestFixture fixture)
24+
{
25+
_scope = fixture.ServiceProvider.CreateScope();
26+
var serviceProvider = _scope.ServiceProvider;
27+
28+
_facilityQueries = serviceProvider.GetRequiredService<IFacilityQueries>();
29+
_dbContext = serviceProvider.GetRequiredService<TenantDbContext>();
30+
}
31+
32+
public void Dispose() => _scope.Dispose();
33+
34+
[Fact]
35+
public async Task GetAsync_CarriesTheVendorsSigningKeySecretId()
36+
{
37+
var facilityId = await CreateFacilityWithVendorAsync(
38+
new VendorAuthenticationSettings { SigningKeySecretId = "epic-signing-key" });
39+
40+
var facility = await _facilityQueries.GetAsync(facilityId, null, CancellationToken.None);
41+
42+
Assert.Equal("epic-signing-key", facility?.Vendor?.Authentication?.SigningKeySecretId);
43+
}
44+
45+
[Fact]
46+
public async Task GetAsync_ReturnsNoSigningKey_WhenTheVendorHasNoneConfigured()
47+
{
48+
var facilityId = await CreateFacilityWithVendorAsync(authentication: null);
49+
50+
var facility = await _facilityQueries.GetAsync(facilityId, null, CancellationToken.None);
51+
52+
Assert.NotNull(facility?.Vendor);
53+
Assert.Null(facility!.Vendor!.Authentication?.SigningKeySecretId);
54+
}
55+
56+
private async Task<string> CreateFacilityWithVendorAsync(VendorAuthenticationSettings? authentication)
57+
{
58+
var vendor = new Vendor
59+
{
60+
Id = Guid.NewGuid(),
61+
Name = $"Vendor-{Guid.NewGuid():N}",
62+
Authentication = authentication
63+
};
64+
var vendorVersion = new VendorVersion
65+
{
66+
Id = Guid.NewGuid(),
67+
VendorId = vendor.Id,
68+
Version = "default"
69+
};
70+
var facility = new Facility
71+
{
72+
Id = Guid.NewGuid(),
73+
FacilityId = $"facility-{Guid.NewGuid():N}",
74+
FacilityName = "Vendor Authentication Test Facility",
75+
TimeZone = "America/Chicago",
76+
VendorVersionId = vendorVersion.Id,
77+
ScheduledReports = new ScheduledReportModel
78+
{
79+
Daily = [],
80+
Weekly = [],
81+
Monthly = []
82+
}
83+
};
84+
85+
await _dbContext.Vendors.AddAsync(vendor);
86+
await _dbContext.VendorVersions.AddAsync(vendorVersion);
87+
await _dbContext.Facilities.AddAsync(facility);
88+
await _dbContext.SaveChangesAsync();
89+
90+
return facility.FacilityId;
91+
}
92+
}

DotNet/ServiceTests/IntegrationTests/Tenant/TenantIntegrationTestFixture.cs

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,9 @@ public TenantIntegrationTestFixture()
121121

122122
builder.Services.AddSingleton(sp => sp.GetRequiredService<IOptions<FacilityIdSettings>>().Value);
123123

124-
// Stub producer for AuditEventCommand
125-
builder.Services.AddSingleton<IProducer<string, AuditEventMessage>>(new StubProducer<string, AuditEventMessage>());
124+
// Recording producer for AuditEventCommand, so tests can assert on emitted audit events.
125+
builder.Services.AddSingleton<RecordingAuditProducer>();
126+
builder.Services.AddSingleton<IProducer<string, AuditEventMessage>>(sp => sp.GetRequiredService<RecordingAuditProducer>());
126127

127128
// Add the real CreateAuditEventCommand
128129
builder.Services.AddSingleton<CreateAuditEventCommand>();
@@ -210,13 +211,58 @@ public Task<string> ExecuteAsync(string signingKey, int expirationMinutes)
210211
}
211212
}
212213

213-
private class StubProducer<TKey, TValue> : IProducer<TKey, TValue>
214+
/// <summary>
215+
/// Captures audit events instead of discarding them. CreateAuditEventCommand.Execute is
216+
/// async void, so a test awaits <see cref="WaitForAsync"/> rather than assuming the
217+
/// message has landed by the time the manager call returns.
218+
/// </summary>
219+
public class RecordingAuditProducer : StubProducer<string, AuditEventMessage>
220+
{
221+
private readonly List<(string Key, AuditEventMessage Value)> _produced = new();
222+
223+
public IReadOnlyList<(string Key, AuditEventMessage Value)> Produced
224+
{
225+
get { lock (_produced) { return _produced.ToList(); } }
226+
}
227+
228+
public void Clear()
229+
{
230+
lock (_produced) { _produced.Clear(); }
231+
}
232+
233+
public override Task<DeliveryResult<string, AuditEventMessage>> ProduceAsync(
234+
string topic, Message<string, AuditEventMessage> message, CancellationToken cancellationToken = default)
235+
{
236+
lock (_produced) { _produced.Add((message.Key, message.Value)); }
237+
return base.ProduceAsync(topic, message, cancellationToken);
238+
}
239+
240+
public async Task<IReadOnlyList<(string Key, AuditEventMessage Value)>> WaitForAsync(
241+
int count, TimeSpan timeout)
242+
{
243+
var deadline = DateTime.UtcNow + timeout;
244+
while (DateTime.UtcNow < deadline)
245+
{
246+
var produced = Produced;
247+
if (produced.Count >= count)
248+
{
249+
return produced;
250+
}
251+
252+
await Task.Delay(20);
253+
}
254+
255+
return Produced;
256+
}
257+
}
258+
259+
public class StubProducer<TKey, TValue> : IProducer<TKey, TValue>
214260
{
215261
public Handle Handle => null;
216262

217263
public string Name => "stub";
218264

219-
public Task<DeliveryResult<TKey, TValue>> ProduceAsync(string topic, Message<TKey, TValue> message, CancellationToken cancellationToken = default)
265+
public virtual Task<DeliveryResult<TKey, TValue>> ProduceAsync(string topic, Message<TKey, TValue> message, CancellationToken cancellationToken = default)
220266
{
221267
return Task.FromResult(new DeliveryResult<TKey, TValue>
222268
{
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
using LantanaGroup.Link.Sdk.ApiClient;
2+
using LantanaGroup.Link.Sdk.Clients;
3+
using LantanaGroup.Link.Shared.Application.Models;
4+
using LantanaGroup.Link.Shared.Application.Models.Integration.Normalization;
5+
using LantanaGroup.Link.Shared.Application.Models.Kafka;
6+
using LantanaGroup.Link.Shared.Application.Models.Tenant;
7+
using LantanaGroup.Link.Tenant.Business.Managers;
8+
using Microsoft.AspNetCore.Http;
9+
using Microsoft.Extensions.DependencyInjection;
10+
using Moq;
11+
using Task = System.Threading.Tasks.Task;
12+
13+
namespace IntegrationTests.Tenant;
14+
15+
[Collection("IntegrationTests")]
16+
[Trait("Category", "IntegrationTests")]
17+
public class VendorAuditTests : IDisposable
18+
{
19+
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(5);
20+
21+
private readonly IServiceScope _scope;
22+
private readonly IVendorManager _vendorManager;
23+
private readonly TenantIntegrationTestFixture.RecordingAuditProducer _auditProducer;
24+
25+
public VendorAuditTests(TenantIntegrationTestFixture fixture)
26+
{
27+
_scope = fixture.ServiceProvider.CreateScope();
28+
var serviceProvider = _scope.ServiceProvider;
29+
30+
_vendorManager = serviceProvider.GetRequiredService<IVendorManager>();
31+
_auditProducer = serviceProvider.GetRequiredService<TenantIntegrationTestFixture.RecordingAuditProducer>();
32+
_auditProducer.Clear();
33+
34+
// Deleting a vendor checks Normalization for references to each of its versions.
35+
var normalizationServiceClient = serviceProvider.GetRequiredService<Mock<INormalizationServiceClient>>();
36+
normalizationServiceClient.Reset();
37+
normalizationServiceClient
38+
.Setup(client => client.GetVendorVersionOperationPresetsAsync(
39+
It.IsAny<Guid>(), null, It.IsAny<CancellationToken>()))
40+
.ReturnsAsync(new LinkApiResponse<List<NormalizationVendorVersionOperationPresetApiModel>>
41+
{
42+
StatusCode = StatusCodes.Status200OK,
43+
Body = []
44+
});
45+
}
46+
47+
public void Dispose() => _scope.Dispose();
48+
49+
[Fact]
50+
public async Task CreateVendor_EmitsACreateAuditEvent()
51+
{
52+
var created = await _vendorManager.CreateVendorAsync(new VendorModel { Name = $"Vendor-{Guid.NewGuid():N}" });
53+
54+
var events = await _auditProducer.WaitForAsync(1, Timeout);
55+
56+
var audit = Assert.Single(events);
57+
Assert.Equal(AuditEventType.Create, audit.Value.Action);
58+
Assert.Equal("Vendor", audit.Value.Resource);
59+
Assert.Equal(created.Id.ToString(), audit.Key);
60+
}
61+
62+
[Fact]
63+
public async Task UpdateVendor_RecordsTheSigningKeySecretIdChange()
64+
{
65+
var created = await _vendorManager.CreateVendorAsync(new VendorModel { Name = $"Vendor-{Guid.NewGuid():N}" });
66+
_auditProducer.Clear();
67+
68+
await _vendorManager.UpdateVendorAsync(created.Id!.Value, new VendorModel
69+
{
70+
Name = created.Name,
71+
Authentication = new VendorAuthenticationSettings { SigningKeySecretId = "epic-signing-key" }
72+
});
73+
74+
var events = await _auditProducer.WaitForAsync(1, Timeout);
75+
76+
var audit = Assert.Single(events);
77+
Assert.Equal(AuditEventType.Update, audit.Value.Action);
78+
var change = Assert.Single(audit.Value.PropertyChanges!,
79+
c => c.PropertyName == nameof(VendorAuthenticationSettings.SigningKeySecretId));
80+
Assert.Null(change.InitialPropertyValue);
81+
Assert.Equal("epic-signing-key", change.NewPropertyValue);
82+
}
83+
84+
[Fact]
85+
public async Task UpdateVendor_RecordsAClearedSigningKeySecretId()
86+
{
87+
var created = await _vendorManager.CreateVendorAsync(new VendorModel
88+
{
89+
Name = $"Vendor-{Guid.NewGuid():N}",
90+
Authentication = new VendorAuthenticationSettings { SigningKeySecretId = "epic-signing-key" }
91+
});
92+
_auditProducer.Clear();
93+
94+
await _vendorManager.UpdateVendorAsync(created.Id!.Value, new VendorModel
95+
{
96+
Name = created.Name,
97+
Authentication = new VendorAuthenticationSettings { SigningKeySecretId = null }
98+
});
99+
100+
var events = await _auditProducer.WaitForAsync(1, Timeout);
101+
102+
var change = Assert.Single(Assert.Single(events).Value.PropertyChanges!,
103+
c => c.PropertyName == nameof(VendorAuthenticationSettings.SigningKeySecretId));
104+
Assert.Equal("epic-signing-key", change.InitialPropertyValue);
105+
Assert.Null(change.NewPropertyValue);
106+
}
107+
108+
[Fact]
109+
public async Task UpdateVendor_ThatChangesNothing_EmitsNoAuditEvent()
110+
{
111+
var created = await _vendorManager.CreateVendorAsync(new VendorModel { Name = $"Vendor-{Guid.NewGuid():N}" });
112+
_auditProducer.Clear();
113+
114+
await _vendorManager.UpdateVendorAsync(created.Id!.Value, new VendorModel { Name = created.Name });
115+
116+
await Task.Delay(200);
117+
118+
Assert.Empty(_auditProducer.Produced);
119+
}
120+
121+
[Fact]
122+
public async Task DeleteVendor_EmitsADeleteAuditEvent()
123+
{
124+
var created = await _vendorManager.CreateVendorAsync(new VendorModel { Name = $"Vendor-{Guid.NewGuid():N}" });
125+
_auditProducer.Clear();
126+
127+
await _vendorManager.DeleteVendorAsync(created.Id!.Value);
128+
129+
var events = await _auditProducer.WaitForAsync(1, Timeout);
130+
131+
Assert.Equal(AuditEventType.Delete, Assert.Single(events).Value.Action);
132+
}
133+
}

DotNet/Shared/Application/Services/ITenantApiService.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,10 @@ public interface ITenantApiService
66
{
77
Task<bool> CheckFacilityExists(string facilityId, CancellationToken cancellationToken = default);
88
Task<FacilityModel> GetFacilityConfig(string facilityId, CancellationToken cancellationToken = default);
9+
10+
/// <summary>
11+
/// Returns the Key Vault secret name holding the signing key for the facility's vendor, or
12+
/// null when the facility has no vendor or that vendor has no key configured.
13+
/// </summary>
14+
Task<string?> GetVendorSigningKeySecretId(string facilityId, CancellationToken cancellationToken = default);
915
}

DotNet/Shared/Application/Services/TenantApiService.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ public async Task<bool> CheckFacilityExists(string facilityId, CancellationToken
8181
throw new Exception($"Error checking if facility ({sanitizedFacilityId}) exists in Tenant Service. Status Code: {response.StatusCode}");
8282
}
8383

84+
public async Task<string?> GetVendorSigningKeySecretId(string facilityId, CancellationToken cancellationToken = default)
85+
{
86+
var facility = await GetFacilityConfig(facilityId, cancellationToken);
87+
88+
return facility?.Vendor?.Authentication?.SigningKeySecretId;
89+
}
90+
8491
public async Task<FacilityModel> GetFacilityConfig(string facilityId, CancellationToken cancellationToken = default)
8592
{
8693
string sanitizedFacilityId = HtmlInputSanitizer.SanitizeAndRemove(facilityId);

0 commit comments

Comments
 (0)