121 lines
3.5 KiB
C#
121 lines
3.5 KiB
C#
using Microsoft.Extensions.Options;
|
|
using StellaOps.Telemetry.Federation.Privacy;
|
|
|
|
namespace StellaOps.Telemetry.Federation.Tests;
|
|
|
|
public sealed class PrivacyBudgetTrackerTests
|
|
{
|
|
private static IOptions<FederatedTelemetryOptions> DefaultOptions(
|
|
double epsilon = 1.0,
|
|
TimeSpan? resetPeriod = null)
|
|
{
|
|
return Options.Create(new FederatedTelemetryOptions
|
|
{
|
|
EpsilonBudget = epsilon,
|
|
BudgetResetPeriod = resetPeriod ?? TimeSpan.FromHours(24)
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Initial_budget_equals_total()
|
|
{
|
|
var tracker = new PrivacyBudgetTracker(DefaultOptions(epsilon: 2.0));
|
|
|
|
Assert.Equal(2.0, tracker.TotalBudget);
|
|
Assert.Equal(2.0, tracker.RemainingEpsilon);
|
|
Assert.False(tracker.IsBudgetExhausted);
|
|
}
|
|
|
|
[Fact]
|
|
public void TrySpend_reduces_remaining_epsilon()
|
|
{
|
|
var tracker = new PrivacyBudgetTracker(DefaultOptions(epsilon: 1.0));
|
|
|
|
Assert.True(tracker.TrySpend(0.3));
|
|
Assert.Equal(0.7, tracker.RemainingEpsilon, precision: 10);
|
|
}
|
|
|
|
[Fact]
|
|
public void TrySpend_rejects_when_budget_exhausted()
|
|
{
|
|
var tracker = new PrivacyBudgetTracker(DefaultOptions(epsilon: 0.5));
|
|
|
|
Assert.True(tracker.TrySpend(0.5));
|
|
Assert.False(tracker.TrySpend(0.1));
|
|
Assert.True(tracker.IsBudgetExhausted);
|
|
}
|
|
|
|
[Fact]
|
|
public void TrySpend_rejects_negative_or_zero_epsilon()
|
|
{
|
|
var tracker = new PrivacyBudgetTracker(DefaultOptions());
|
|
|
|
Assert.False(tracker.TrySpend(0));
|
|
Assert.False(tracker.TrySpend(-0.5));
|
|
}
|
|
|
|
[Fact]
|
|
public void Reset_restores_full_budget()
|
|
{
|
|
var tracker = new PrivacyBudgetTracker(DefaultOptions(epsilon: 1.0));
|
|
|
|
tracker.TrySpend(0.8);
|
|
Assert.Equal(0.2, tracker.RemainingEpsilon, precision: 10);
|
|
|
|
tracker.Reset();
|
|
Assert.Equal(1.0, tracker.RemainingEpsilon);
|
|
Assert.False(tracker.IsBudgetExhausted);
|
|
}
|
|
|
|
[Fact]
|
|
public void Snapshot_tracks_queries_and_suppressed_counts()
|
|
{
|
|
var tracker = new PrivacyBudgetTracker(DefaultOptions(epsilon: 0.5));
|
|
|
|
tracker.TrySpend(0.2); // success
|
|
tracker.TrySpend(0.2); // success
|
|
tracker.TrySpend(0.2); // fails — over budget
|
|
|
|
var snapshot = tracker.GetSnapshot();
|
|
Assert.Equal(2, snapshot.QueriesThisPeriod);
|
|
Assert.Equal(1, snapshot.SuppressedThisPeriod);
|
|
Assert.True(snapshot.Exhausted);
|
|
}
|
|
|
|
[Fact]
|
|
public void LaplacianNoise_produces_finite_values()
|
|
{
|
|
var rng = new Random(42);
|
|
for (int i = 0; i < 1000; i++)
|
|
{
|
|
var noise = PrivacyBudgetTracker.LaplacianNoise(1.0, 0.5, rng);
|
|
Assert.True(double.IsFinite(noise));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void LaplacianNoise_is_deterministic_with_fixed_seed()
|
|
{
|
|
var rng1 = new Random(12345);
|
|
var rng2 = new Random(12345);
|
|
|
|
var noise1 = PrivacyBudgetTracker.LaplacianNoise(1.0, 1.0, rng1);
|
|
var noise2 = PrivacyBudgetTracker.LaplacianNoise(1.0, 1.0, rng2);
|
|
|
|
Assert.Equal(noise1, noise2);
|
|
}
|
|
|
|
[Fact]
|
|
public void LaplacianNoise_scales_with_sensitivity_and_epsilon()
|
|
{
|
|
var rng = new Random(42);
|
|
var samples = Enumerable.Range(0, 10000)
|
|
.Select(_ => PrivacyBudgetTracker.LaplacianNoise(1.0, 1.0, rng))
|
|
.ToList();
|
|
|
|
// Mean should be approximately 0 for large sample
|
|
var mean = samples.Average();
|
|
Assert.True(Math.Abs(mean) < 0.1, $"Mean {mean} too far from 0");
|
|
}
|
|
}
|