The SUPER-role "preview results" QA tooling seeds a completed ExamSimulation with scripted (not real) answers, flagged IsSample. The dashboard's readiness lookup picked whichever completed simulation was most recent regardless of that flag, so running the QA preview on an account could override its real Exam Readiness card with a synthetic pass probability. Recent-activity/weekly-activity queries got the same exclusion since they read from the same collection for the same reason. Co-Authored-By: Claude Sonnet 5 <[email protected]>
234 lines
8.6 KiB
C#
234 lines
8.6 KiB
C#
using QuestionsApi.Models;
|
|
using QuestionsApi.Services;
|
|
using Xunit;
|
|
|
|
namespace QuestionsApi.Tests;
|
|
|
|
public sealed class DashboardTests
|
|
{
|
|
[Fact]
|
|
public void PercentageRoundsToNearestWholeNumber()
|
|
{
|
|
Assert.Equal(67, DashboardStore.Percentage(2, 3));
|
|
Assert.Equal(0, DashboardStore.Percentage(0, 0));
|
|
}
|
|
|
|
[Fact]
|
|
public void StreakIncludesYesterdayWhenUserHasNotStudiedToday()
|
|
{
|
|
var today = new DateTime(2026, 7, 18, 0, 0, 0, DateTimeKind.Utc);
|
|
var dates = new[] { today.AddDays(-1), today.AddDays(-2), today.AddDays(-3), today.AddDays(-5) };
|
|
Assert.Equal(3, DashboardStore.CalculateCurrentStreak(dates, today));
|
|
}
|
|
|
|
[Fact]
|
|
public void StreakStopsAtFirstMissingDay()
|
|
{
|
|
var today = new DateTime(2026, 7, 18, 0, 0, 0, DateTimeKind.Utc);
|
|
var dates = new[] { today, today.AddDays(-1), today.AddDays(-3) };
|
|
Assert.Equal(2, DashboardStore.CalculateCurrentStreak(dates, today));
|
|
}
|
|
|
|
[Fact]
|
|
public void SystemPerformanceReturnsEveryOfficialSystemInOrder()
|
|
{
|
|
var attempts = new[]
|
|
{
|
|
Attempt("cardio-1", true),
|
|
Attempt("cardio-2", false),
|
|
Attempt("ethics-1", true),
|
|
Attempt("ignored", true),
|
|
Attempt("skipped-cardio", false, true)
|
|
};
|
|
var questions = new Dictionary<string, Question>
|
|
{
|
|
["cardio-1"] = Question("Cardiology"),
|
|
["cardio-2"] = Question("Cardiovascular System"),
|
|
["ethics-1"] = Question("Medical Ethics"),
|
|
["ignored"] = Question("Unmapped Specialty"),
|
|
["skipped-cardio"] = Question("Cardiology")
|
|
};
|
|
|
|
var result = DashboardStore.BuildSystemPerformance(attempts, questions);
|
|
|
|
Assert.Equal(14, result.Count);
|
|
Assert.Equal(DashboardStore.Step3Systems, result.Select(value => value.System));
|
|
var cardiovascular = result.Single(value => value.System == "Cardiovascular System");
|
|
Assert.Equal(1, cardiovascular.Correct);
|
|
Assert.Equal(2, cardiovascular.Total);
|
|
Assert.Equal(50, cardiovascular.Accuracy);
|
|
var social = result.Single(value => value.System.StartsWith("Social Sciences"));
|
|
Assert.Equal(1, social.Correct);
|
|
Assert.Equal(1, social.Total);
|
|
Assert.All(result.Where(value => value.System != cardiovascular.System && value.System != social.System),
|
|
value => Assert.Equal(0, value.Total));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("Preventive Medicine", "Human Development")]
|
|
[InlineData("Infectious Disease", "Immune System, Blood & Lymphoreticular System, and Multisystem Processes/Disorders")]
|
|
[InlineData("Dermatology", "Skin & Subcutaneous Tissue")]
|
|
[InlineData("Pulmonary", "Respiratory System")]
|
|
[InlineData("Pregnancy", "Pregnancy/Childbirth & Female Reproductive System & Breast")]
|
|
[InlineData("Biostatistics", "Biostatistics & Epidemiology/Population Health & Interpretation of the Medical Literature")]
|
|
[InlineData("Medical Ethics", "Social Sciences: Communication Skills/Ethics/Patient Safety")]
|
|
public void SystemAliasesMapToOfficialCategories(string source, string expected)
|
|
{
|
|
Assert.Equal(expected, DashboardStore.NormalizeStep3System(source));
|
|
}
|
|
|
|
[Fact]
|
|
public void TrendCarriesRatingForwardAndLeavesInactiveAccuracyAsGapData()
|
|
{
|
|
var today = new DateTime(2026, 7, 25, 0, 0, 0, DateTimeKind.Utc);
|
|
var baselineDate = today.AddDays(-2).AddHours(8);
|
|
var attempts = new[]
|
|
{
|
|
Attempt("one", true, createdUtc: today.AddDays(-2).AddHours(9),
|
|
rating: 1018, ratingUtc: today.AddDays(-2).AddHours(9)),
|
|
Attempt("two", false, createdUtc: today.AddDays(-2).AddHours(10),
|
|
rating: 1002, ratingUtc: today.AddDays(-2).AddHours(10)),
|
|
Attempt("three", true, createdUtc: today.AddHours(11),
|
|
rating: 1020, ratingUtc: today.AddHours(11)),
|
|
Attempt("skip", false, true, today.AddDays(-1).AddHours(4),
|
|
990, today.AddDays(-1).AddHours(4))
|
|
};
|
|
var rating = new UserRating
|
|
{
|
|
OverallElo = 1020,
|
|
HistoryBaselineElo = 1000,
|
|
HistoryStartedUtc = baselineDate
|
|
};
|
|
|
|
var result = DashboardStore.BuildPerformanceTrend(
|
|
attempts, rating, Array.Empty<CcsAttempt>(), new CcsUserRating(), today);
|
|
|
|
Assert.Equal(30, result.Count);
|
|
Assert.Null(result[^4].QuestionBankRating);
|
|
Assert.Equal(1002, result[^3].QuestionBankRating);
|
|
Assert.Equal(50, result[^3].Accuracy);
|
|
Assert.Equal(2, result[^3].Total);
|
|
Assert.Equal(990, result[^2].QuestionBankRating);
|
|
Assert.Equal(0, result[^2].Total);
|
|
Assert.Equal(1020, result[^1].QuestionBankRating);
|
|
Assert.Equal(100, result[^1].Accuracy);
|
|
}
|
|
|
|
[Fact]
|
|
public void TrendCarriesCcsRatingForwardIndependentlyOfQuestionBankRating()
|
|
{
|
|
var today = new DateTime(2026, 7, 25, 0, 0, 0, DateTimeKind.Utc);
|
|
var baselineDate = today.AddDays(-2).AddHours(8);
|
|
var rating = new UserRating();
|
|
var ccsRating = new CcsUserRating
|
|
{
|
|
Elo = 1050,
|
|
HistoryBaselineElo = 1000,
|
|
HistoryStartedUtc = baselineDate
|
|
};
|
|
var ccsAttempts = new[]
|
|
{
|
|
RatedCcsAttempt("one", today.AddDays(-1).AddHours(9), 1030),
|
|
RatedCcsAttempt("two", today.AddHours(10), 1050)
|
|
};
|
|
|
|
var result = DashboardStore.BuildPerformanceTrend(
|
|
Array.Empty<QuestionAttempt>(), rating, ccsAttempts, ccsRating, today);
|
|
|
|
Assert.Equal(30, result.Count);
|
|
Assert.Equal(1000, result[^3].CcsRating);
|
|
Assert.Equal(1030, result[^2].CcsRating);
|
|
Assert.Equal(1050, result[^1].CcsRating);
|
|
}
|
|
|
|
private static CcsAttempt RatedCcsAttempt(string caseExternalId, DateTime appliedUtc, int userElo) => new()
|
|
{
|
|
CaseExternalId = caseExternalId,
|
|
Rating = new CcsRatingChange { Rated = true, UserElo = userElo, AppliedUtc = appliedUtc }
|
|
};
|
|
|
|
private static QuestionAttempt Attempt(
|
|
string questionId,
|
|
bool correct,
|
|
bool skip = false,
|
|
DateTime? createdUtc = null,
|
|
int? rating = null,
|
|
DateTime? ratingUtc = null) => new()
|
|
{
|
|
QuestionExternalId = questionId,
|
|
IsCorrect = correct,
|
|
IsSkip = skip,
|
|
CreatedUtc = createdUtc ?? DateTime.UtcNow,
|
|
UserOverallEloAfter = rating,
|
|
RatingAppliedUtc = ratingUtc
|
|
};
|
|
|
|
private static Question Question(string system) => new()
|
|
{
|
|
ExternalId = system,
|
|
System = system
|
|
};
|
|
|
|
[Fact]
|
|
public void PercentileReturnsNullWithNoUsers()
|
|
{
|
|
Assert.Null(DashboardStore.Percentile(0, 0));
|
|
}
|
|
|
|
[Fact]
|
|
public void PercentileComputesTopPercentFromUsersRatedHigher()
|
|
{
|
|
// 18 of 100 users rate higher than this user - "top 18%"
|
|
Assert.Equal(18, DashboardStore.Percentile(18, 100));
|
|
}
|
|
|
|
[Fact]
|
|
public void PercentileNeverReturnsZero()
|
|
{
|
|
// Even the single best-rated user should read as "top 1%", not "top 0%"
|
|
Assert.Equal(1, DashboardStore.Percentile(0, 100));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0.85, "High")]
|
|
[InlineData(0.7, "High")]
|
|
[InlineData(0.55, "Medium")]
|
|
[InlineData(0.4, "Medium")]
|
|
[InlineData(0.1, "Low")]
|
|
public void ReadinessLabelUsesPassProbabilityThresholds(double passProbability, string expected)
|
|
{
|
|
Assert.Equal(expected, DashboardStore.ReadinessLabel(passProbability));
|
|
}
|
|
|
|
[Fact]
|
|
public void SelectLatestReadinessScoreSkipsSampleSimulationsEvenWhenMostRecent()
|
|
{
|
|
var today = new DateTime(2026, 7, 25, 0, 0, 0, DateTimeKind.Utc);
|
|
var simulations = new[]
|
|
{
|
|
CompletedSimulation(isSample: true, completedUtc: today, passProbability: 0.98),
|
|
CompletedSimulation(isSample: false, completedUtc: today.AddDays(-3), passProbability: 0.12)
|
|
};
|
|
|
|
var score = DashboardStore.SelectLatestReadinessScore(simulations);
|
|
|
|
Assert.NotNull(score);
|
|
Assert.Equal(0.12, score.PassProbability);
|
|
}
|
|
|
|
[Fact]
|
|
public void SelectLatestReadinessScoreReturnsNullWhenOnlySamplesExist()
|
|
{
|
|
var simulations = new[] { CompletedSimulation(isSample: true, completedUtc: DateTime.UtcNow, passProbability: 0.98) };
|
|
|
|
Assert.Null(DashboardStore.SelectLatestReadinessScore(simulations));
|
|
}
|
|
|
|
private static ExamSimulation CompletedSimulation(bool isSample, DateTime completedUtc, double passProbability) => new()
|
|
{
|
|
IsSample = isSample,
|
|
CompletedUtc = completedUtc,
|
|
Score = new ExamSimulationScore { PassProbability = passProbability }
|
|
};
|
|
}
|