Template
303 lines
14 KiB
C#
303 lines
14 KiB
C#
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using Moq;
|
|
using Strata.ContinuousImprovement.Biz.DbContexts;
|
|
using Strata.ContinuousImprovement.Biz.Exploration;
|
|
using Strata.ContinuousImprovement.Biz.Exploration.Filters;
|
|
using Strata.ContinuousImprovement.Biz.FiscalMonths;
|
|
using Strata.ContinuousImprovement.Biz.Test.Unit.Utilities;
|
|
using Strata.ContinuousImprovement.Biz.Utilities;
|
|
using Strata.Schema.Client;
|
|
using Strata.Schema.Client.Dtos;
|
|
using Strata.Schema.Client.Dtos.Info;
|
|
using Strata.SqlTools.Configuration.Common.AsyncFactory;
|
|
using Strata.StrataSphereCompare.Client;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using VerifyNUnit;
|
|
|
|
namespace Strata.ContinuousImprovement.Biz.Test.Integration.SnowflakeIntegrationTests
|
|
{
|
|
/// <summary>
|
|
/// Baseline regression tests for the exploration query refactor.
|
|
///
|
|
/// WORKFLOW:
|
|
/// 1. On main (or a branch from main): run these tests to generate golden snapshots.
|
|
/// Commit the resulting *.verified.txt files.
|
|
/// 2. On the refactor branch: run the same tests.
|
|
/// Verify diffs output against the golden snapshots — any divergence fails.
|
|
/// </summary>
|
|
[ExcludeFromCodeCoverage]
|
|
[TestFixture, Category("Integration"), Category("CCI")]
|
|
public class FlexibleExplorationBaselineTest : IntegrationTestBase
|
|
{
|
|
private static readonly Guid _configurationGuid = Guid.Parse("aa4cba4f-2506-462c-8e34-c45b21653988");
|
|
|
|
private ExplorationDetailInfo _detailInfo;
|
|
|
|
[OneTimeSetUp]
|
|
public async Task SetupDetailInfo()
|
|
{
|
|
var summaryInfo = CreateSummaryInfo();
|
|
var summaries = await ExplorationService.GetCaseTypeFamilySummariesAsync(summaryInfo, CancellationToken.None);
|
|
|
|
if (!summaries.Any())
|
|
throw new InvalidOperationException("No Case Type Family summaries returned — cannot run baseline tests.");
|
|
|
|
// Configure the schema service mock so the Gender ScoreMember filter resolves to
|
|
// a real SQL fragment (fpes.GenderID IN (1)) rather than being silently skipped.
|
|
// GenderID 1 = Male in the GM Automation test database.
|
|
var genderDimensionGuid = Guid.NewGuid();
|
|
Mock.Get(SchemaServiceClient)
|
|
.Setup(s => s.GetDimensionByGlobalIdAsync("Gender", It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ScoreDimensionInfoDto { DimensionGuid = genderDimensionGuid });
|
|
Mock.Get(SchemaServiceClient)
|
|
.Setup(s => s.GetDimensionMembersFromHierarchyPathsAsync(genderDimensionGuid, It.IsAny<IEnumerable<string>>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new[] { new MemberDto { Id = "1" } });
|
|
|
|
// Rebuild ExplorationFilterService and ExplorationService with the configured mock
|
|
// so the gender filter is active for all tests in this fixture.
|
|
var filterService = new ExplorationFilterService(
|
|
ServiceProvider.GetRequiredService<IAsyncDbContextFactory<JazzDbContext>>(),
|
|
SchemaServiceClient,
|
|
Mock.Of<ILogger<ExplorationFilterService>>());
|
|
var featureFlagWrapper = Mock.Of<IFeatureFlagWrapper>();
|
|
Mock.Get(featureFlagWrapper)
|
|
.Setup(f => f.IsCiBenchmarkingEnabled(It.IsAny<bool>()))
|
|
.ReturnsAsync(true);
|
|
ExplorationService = new ExplorationService(
|
|
JazzConnBuilderFactory,
|
|
SnowflakeDatabaseContext,
|
|
Mock.Of<IFiscalMonthResolver>(),
|
|
featureFlagWrapper,
|
|
Mock.Of<IStrataSphereCompareService>(),
|
|
filterService,
|
|
Mock.Of<ILogger<ExplorationService>>());
|
|
|
|
// Always pick the same CTF (highest encounter count) so snapshots are deterministic.
|
|
var busiest = summaries.OrderByDescending(s => s.Data.TotalEncounters).First();
|
|
|
|
// Derive a real cost driver from the CTF's detail breakdown so the
|
|
// encounter query (which filters on cc.costdriver = @CostDriver) returns data.
|
|
var costDriverInfo = CreateSummaryInfo();
|
|
costDriverInfo.CaseTypeFamilyId = busiest.Data.Id;
|
|
var costDriverDetails = await ExplorationService.GetCaseTypeFamilySummaryDetailsAsync(costDriverInfo, CancellationToken.None);
|
|
var costDriver = costDriverDetails
|
|
.SelectMany(d => d.Children ?? [])
|
|
.OrderByDescending(c => c.Data.TotalEncounters)
|
|
.FirstOrDefault()?.Name
|
|
?? throw new InvalidOperationException("No cost driver children found for the busiest CTF.");
|
|
|
|
_detailInfo = new ExplorationDetailInfo
|
|
{
|
|
ConfigurationGuid = _configurationGuid,
|
|
CaseTypeFamilyId = busiest.Data.Id,
|
|
CostDriver = costDriver,
|
|
ExclusionCriteria = new ExclusionCriteria(),
|
|
ExcludedEncounters = new List<long>(),
|
|
IncludedEncounters = new List<long>(),
|
|
Filters = new Biz.Exploration.Filters.Filter
|
|
{
|
|
DischargeDateStart = new DateTime(2024, 1, 1),
|
|
DischargeDateEnd = new DateTime(2025, 1, 1),
|
|
FilterChipItems = new List<FilterChipItem>
|
|
{
|
|
new FilterChipItem
|
|
{
|
|
ChipType = ChipType.DateRange,
|
|
Key = ChipKey.DischargeDateID,
|
|
Filters = [],
|
|
DateRange = [new DateTime(2024, 1, 1), new DateTime(2025, 1, 1)]
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// ── No filters ──────────────────────────────────────────────────────────
|
|
|
|
[Test]
|
|
public async Task GetCaseTypeFamilyDetailsAsync_MatchesBaseline()
|
|
{
|
|
var result = await ExplorationService.GetCaseTypeFamilyDetailsAsync(_detailInfo, CancellationToken.None);
|
|
|
|
await Verifier.Verify(result, verifySettings)
|
|
.UseFileName(TestContext.CurrentContext.TestName());
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetEncountersAsync_MatchesBaseline()
|
|
{
|
|
var result = await ExplorationService.GetEncountersAsync(_detailInfo, _configurationGuid, CancellationToken.None);
|
|
|
|
await Verifier.Verify(result.OrderBy(e => e.EncounterId), verifySettings)
|
|
.UseFileName(TestContext.CurrentContext.TestName());
|
|
}
|
|
|
|
// ── Boolean filter (charges > 0) ─────────────────────────────────────
|
|
// IsGreaterThanZeroCharges hits the queryFilter path and reliably
|
|
// produces a meaningful subset without risking empty results.
|
|
|
|
[Test]
|
|
public async Task GetCaseTypeFamilyDetailsAsync_WithChargesFilter_MatchesBaseline()
|
|
{
|
|
var info = CreateDetailInfo(extraFilter: CreateChargesFilter());
|
|
|
|
var result = await ExplorationService.GetCaseTypeFamilyDetailsAsync(info, CancellationToken.None);
|
|
|
|
await Verifier.Verify(result, verifySettings)
|
|
.UseFileName(TestContext.CurrentContext.TestName());
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetEncountersAsync_WithChargesFilter_MatchesBaseline()
|
|
{
|
|
var info = CreateDetailInfo(extraFilter: CreateChargesFilter());
|
|
|
|
var result = await ExplorationService.GetEncountersAsync(info, _configurationGuid, CancellationToken.None);
|
|
|
|
await Verifier.Verify(result.OrderBy(e => e.EncounterId), verifySettings)
|
|
.UseFileName(TestContext.CurrentContext.TestName());
|
|
}
|
|
|
|
// ── FPES ScoreMember filter (gender) ────────────────────────────────
|
|
// Gender is an FPES-level filter — it resolves to fpes.GenderID IN (...)
|
|
// via the schema service and lands in the encounterSummaryFilter placeholder.
|
|
|
|
[Test]
|
|
public async Task GetCaseTypeFamilyDetailsAsync_WithGenderFilter_MatchesBaseline()
|
|
{
|
|
var info = CreateDetailInfo(extraFilter: CreateGenderFilter());
|
|
|
|
var result = await ExplorationService.GetCaseTypeFamilyDetailsAsync(info, CancellationToken.None);
|
|
|
|
await Verifier.Verify(result, verifySettings)
|
|
.UseFileName(TestContext.CurrentContext.TestName());
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetEncountersAsync_WithGenderFilter_MatchesBaseline()
|
|
{
|
|
var info = CreateDetailInfo(extraFilter: CreateGenderFilter());
|
|
|
|
var result = await ExplorationService.GetEncountersAsync(info, _configurationGuid, CancellationToken.None);
|
|
|
|
await Verifier.Verify(result.OrderBy(e => e.EncounterId), verifySettings)
|
|
.UseFileName(TestContext.CurrentContext.TestName());
|
|
}
|
|
|
|
// ── IQR exclusions ───────────────────────────────────────────────────
|
|
|
|
[Test]
|
|
public async Task GetCaseTypeFamilyDetailsAsync_WithIqrExclusions_MatchesBaseline()
|
|
{
|
|
var info = CreateDetailInfo(exclusion: new ExclusionCriteria
|
|
{
|
|
CostExclusionType = ExclusionCriteriaType.Iqr,
|
|
CostExclusionValue = 1.5,
|
|
LosExclusionType = ExclusionCriteriaType.Iqr,
|
|
LosExclusionValue = 1.5
|
|
});
|
|
|
|
var result = await ExplorationService.GetCaseTypeFamilyDetailsAsync(info, CancellationToken.None);
|
|
|
|
await Verifier.Verify(result, verifySettings)
|
|
.UseFileName(TestContext.CurrentContext.TestName());
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetEncountersAsync_WithIqrExclusions_MatchesBaseline()
|
|
{
|
|
var info = CreateDetailInfo(exclusion: new ExclusionCriteria
|
|
{
|
|
CostExclusionType = ExclusionCriteriaType.Iqr,
|
|
CostExclusionValue = 1.5,
|
|
LosExclusionType = ExclusionCriteriaType.Iqr,
|
|
LosExclusionValue = 1.5
|
|
});
|
|
|
|
var result = await ExplorationService.GetEncountersAsync(info, _configurationGuid, CancellationToken.None);
|
|
|
|
await Verifier.Verify(result.OrderBy(e => e.EncounterId), verifySettings)
|
|
.UseFileName(TestContext.CurrentContext.TestName());
|
|
}
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
|
|
private ExplorationDetailInfo CreateDetailInfo(
|
|
FilterChipItem extraFilter = null,
|
|
ExclusionCriteria exclusion = null)
|
|
{
|
|
var dateFilter = new FilterChipItem
|
|
{
|
|
ChipType = ChipType.DateRange,
|
|
Key = ChipKey.DischargeDateID,
|
|
Filters = [],
|
|
DateRange = [new DateTime(2024, 1, 1), new DateTime(2025, 1, 1)]
|
|
};
|
|
|
|
var chips = extraFilter != null
|
|
? new List<FilterChipItem> { dateFilter, extraFilter }
|
|
: new List<FilterChipItem> { dateFilter };
|
|
|
|
return new ExplorationDetailInfo
|
|
{
|
|
ConfigurationGuid = _detailInfo.ConfigurationGuid,
|
|
CaseTypeFamilyId = _detailInfo.CaseTypeFamilyId,
|
|
CostDriver = _detailInfo.CostDriver,
|
|
ExclusionCriteria = exclusion ?? new ExclusionCriteria(),
|
|
ExcludedEncounters = new List<long>(),
|
|
IncludedEncounters = new List<long>(),
|
|
Filters = new Biz.Exploration.Filters.Filter
|
|
{
|
|
DischargeDateStart = new DateTime(2024, 1, 1),
|
|
DischargeDateEnd = new DateTime(2025, 1, 1),
|
|
FilterChipItems = chips
|
|
}
|
|
};
|
|
}
|
|
|
|
// Gender filter — FPES-level (encounterSummaryFilter placeholder).
|
|
// GlobalId "Gender" maps to the Gender dimension in the schema service.
|
|
// The Filters list contains the hierarchy path ID for "Male" as resolved
|
|
// by the schema service; adjust the Id if needed for your test database.
|
|
private static FilterChipItem CreateGenderFilter() => new()
|
|
{
|
|
ChipType = ChipType.ScoreMember,
|
|
GlobalId = "Gender",
|
|
Key = ChipKey.GenderID,
|
|
Operator = OperatorType.In,
|
|
Filters = new List<TreeNode> { new() { Id = "Male", DisplayText = "Male" } }
|
|
};
|
|
|
|
private static FilterChipItem CreateChargesFilter() => new()
|
|
{
|
|
ChipType = ChipType.Boolean,
|
|
Key = ChipKey.IsGreaterThanZeroCharges,
|
|
IsSelected = true,
|
|
Operator = OperatorType.In,
|
|
Filters = new List<TreeNode>()
|
|
};
|
|
|
|
private ExplorationCostDriverInfo CreateSummaryInfo() => new()
|
|
{
|
|
ConfigurationGuid = _configurationGuid,
|
|
ExclusionCriteria = new ExclusionCriteria(),
|
|
Filters = new Biz.Exploration.Filters.Filter
|
|
{
|
|
DischargeDateStart = new DateTime(2024, 1, 1),
|
|
DischargeDateEnd = new DateTime(2025, 1, 1),
|
|
FilterChipItems = new List<FilterChipItem>
|
|
{
|
|
new FilterChipItem
|
|
{
|
|
ChipType = ChipType.DateRange,
|
|
Key = ChipKey.DischargeDateID,
|
|
Filters = [],
|
|
DateRange = [new DateTime(2024, 1, 1), new DateTime(2025, 1, 1)]
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|