diff --git a/src/SpamGuard/State/TrustedSenderStore.cs b/src/SpamGuard/State/TrustedSenderStore.cs new file mode 100644 index 0000000..b52e005 --- /dev/null +++ b/src/SpamGuard/State/TrustedSenderStore.cs @@ -0,0 +1,68 @@ +// src/SpamGuard/State/TrustedSenderStore.cs +namespace SpamGuard.State; + +using System.Text.Json; + +public sealed class TrustedSenderStore +{ + private readonly string _filePath; + private readonly HashSet _senders; + private readonly object _lock = new(); + + public TrustedSenderStore(string dataDirectory) + { + _filePath = Path.Combine(dataDirectory, "trusted-senders.json"); + _senders = Load(); + } + + public int Count + { + get { lock (_lock) { return _senders.Count; } } + } + + public bool IsTrusted(string email) + { + lock (_lock) + { + return _senders.Contains(Normalize(email)); + } + } + + public void Add(string email) + { + lock (_lock) + { + _senders.Add(Normalize(email)); + } + } + + public void AddRange(IEnumerable emails) + { + lock (_lock) + { + foreach (var email in emails) + _senders.Add(Normalize(email)); + } + } + + public void Save() + { + lock (_lock) + { + var json = JsonSerializer.Serialize(_senders); + File.WriteAllText(_filePath, json); + } + } + + private static string Normalize(string email) => email.Trim().ToLowerInvariant(); + + private HashSet Load() + { + if (!File.Exists(_filePath)) + return new HashSet(); + + var json = File.ReadAllText(_filePath); + return JsonSerializer.Deserialize>(json) + ?? new HashSet(); + } +} diff --git a/tests/SpamGuard.Tests/State/TrustedSenderStoreTests.cs b/tests/SpamGuard.Tests/State/TrustedSenderStoreTests.cs new file mode 100644 index 0000000..13da146 --- /dev/null +++ b/tests/SpamGuard.Tests/State/TrustedSenderStoreTests.cs @@ -0,0 +1,79 @@ +// tests/SpamGuard.Tests/State/TrustedSenderStoreTests.cs +namespace SpamGuard.Tests.State; + +using SpamGuard.State; + +public class TrustedSenderStoreTests : IDisposable +{ + private readonly string _tempDir; + private readonly TrustedSenderStore _store; + + public TrustedSenderStoreTests() + { + _tempDir = Path.Combine(Path.GetTempPath(), $"spamguard-test-{Guid.NewGuid()}"); + Directory.CreateDirectory(_tempDir); + _store = new TrustedSenderStore(_tempDir); + } + + public void Dispose() + { + if (Directory.Exists(_tempDir)) + Directory.Delete(_tempDir, true); + } + + [Fact] + public void IsTrusted_ReturnsFalse_WhenEmpty() + { + Assert.False(_store.IsTrusted("someone@example.com")); + } + + [Fact] + public void IsTrusted_ReturnsTrue_AfterAdd() + { + _store.Add("someone@example.com"); + Assert.True(_store.IsTrusted("someone@example.com")); + } + + [Fact] + public void IsTrusted_IsCaseInsensitive() + { + _store.Add("Someone@Example.COM"); + Assert.True(_store.IsTrusted("someone@example.com")); + } + + [Fact] + public void IsTrusted_TrimsWhitespace() + { + _store.Add(" someone@example.com "); + Assert.True(_store.IsTrusted("someone@example.com")); + } + + [Fact] + public void AddRange_AddsMultiple() + { + _store.AddRange(["a@b.com", "c@d.com", "e@f.com"]); + Assert.True(_store.IsTrusted("a@b.com")); + Assert.True(_store.IsTrusted("c@d.com")); + Assert.True(_store.IsTrusted("e@f.com")); + } + + [Fact] + public void Save_And_Load_RoundTrips() + { + _store.Add("a@b.com"); + _store.Add("c@d.com"); + _store.Save(); + + var loaded = new TrustedSenderStore(_tempDir); + Assert.True(loaded.IsTrusted("a@b.com")); + Assert.True(loaded.IsTrusted("c@d.com")); + Assert.False(loaded.IsTrusted("x@y.com")); + } + + [Fact] + public void Count_ReturnsCorrectValue() + { + _store.AddRange(["a@b.com", "c@d.com"]); + Assert.Equal(2, _store.Count); + } +}