2026-04-07 11:41:45 +01:00
|
|
|
// tests/SpamGuard.Tests/Services/EmailClassifierTests.cs
|
|
|
|
|
namespace SpamGuard.Tests.Services;
|
|
|
|
|
|
|
|
|
|
using System.Net;
|
|
|
|
|
using System.Text.Json;
|
|
|
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
|
|
|
using Microsoft.Extensions.Options;
|
|
|
|
|
using SpamGuard.Configuration;
|
|
|
|
|
using SpamGuard.Models;
|
|
|
|
|
using SpamGuard.Services;
|
2026-04-12 09:13:43 +01:00
|
|
|
using SpamGuard.State;
|
2026-04-07 11:41:45 +01:00
|
|
|
|
|
|
|
|
public class EmailClassifierTests
|
|
|
|
|
{
|
|
|
|
|
private static SpamGuardOptions DefaultOptions => new()
|
|
|
|
|
{
|
|
|
|
|
Claude = new ClaudeOptions
|
|
|
|
|
{
|
|
|
|
|
ApiKey = "test-key",
|
|
|
|
|
Model = "claude-sonnet-4-6",
|
|
|
|
|
MaxBodyLength = 2000
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private static EmailSummary SampleEmail => new(
|
|
|
|
|
Uid: 1,
|
|
|
|
|
From: "spammer@sketchy.com",
|
|
|
|
|
Subject: "Buy now! Limited offer!",
|
|
|
|
|
BodySnippet: "Click here to claim your prize...",
|
|
|
|
|
Date: DateTimeOffset.UtcNow
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void BuildPrompt_ContainsSenderAndSubjectAndBody()
|
|
|
|
|
{
|
|
|
|
|
var classifier = new EmailClassifier(
|
|
|
|
|
Options.Create(DefaultOptions),
|
|
|
|
|
new NullLogger<EmailClassifier>(),
|
2026-04-12 09:13:43 +01:00
|
|
|
new HttpClient(),
|
|
|
|
|
new OverrideStore(Path.GetTempPath())
|
2026-04-07 11:41:45 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
var prompt = classifier.BuildPrompt(SampleEmail);
|
|
|
|
|
|
|
|
|
|
Assert.Contains("spammer@sketchy.com", prompt);
|
|
|
|
|
Assert.Contains("Buy now! Limited offer!", prompt);
|
|
|
|
|
Assert.Contains("Click here to claim your prize...", prompt);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void BuildPrompt_TruncatesLongBody()
|
|
|
|
|
{
|
|
|
|
|
var longBody = new string('x', 5000);
|
|
|
|
|
var email = SampleEmail with { BodySnippet = longBody };
|
|
|
|
|
|
|
|
|
|
var classifier = new EmailClassifier(
|
|
|
|
|
Options.Create(DefaultOptions),
|
|
|
|
|
new NullLogger<EmailClassifier>(),
|
2026-04-12 09:13:43 +01:00
|
|
|
new HttpClient(),
|
|
|
|
|
new OverrideStore(Path.GetTempPath())
|
2026-04-07 11:41:45 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
var prompt = classifier.BuildPrompt(email);
|
|
|
|
|
|
|
|
|
|
// Body in prompt should be truncated to MaxBodyLength
|
|
|
|
|
Assert.DoesNotContain(longBody, prompt);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ParseResponse_ValidJson_ReturnsResult()
|
|
|
|
|
{
|
|
|
|
|
var json = """{"classification": "spam", "confidence": 0.95, "reason": "Unsolicited marketing"}""";
|
|
|
|
|
|
|
|
|
|
var result = EmailClassifier.ParseResponse(json);
|
|
|
|
|
|
|
|
|
|
Assert.NotNull(result);
|
|
|
|
|
Assert.True(result.IsSpam);
|
|
|
|
|
Assert.Equal(0.95, result.Confidence);
|
|
|
|
|
Assert.Equal("Unsolicited marketing", result.Reason);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ParseResponse_InvalidJson_ReturnsNull()
|
|
|
|
|
{
|
|
|
|
|
var result = EmailClassifier.ParseResponse("not json at all");
|
|
|
|
|
Assert.Null(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ParseResponse_JsonWithMarkdownFencing_ReturnsResult()
|
|
|
|
|
{
|
|
|
|
|
var json = """
|
|
|
|
|
```json
|
|
|
|
|
{"classification": "legitimate", "confidence": 0.85, "reason": "Normal business email"}
|
|
|
|
|
```
|
|
|
|
|
""";
|
|
|
|
|
|
|
|
|
|
var result = EmailClassifier.ParseResponse(json);
|
|
|
|
|
|
|
|
|
|
Assert.NotNull(result);
|
|
|
|
|
Assert.False(result.IsSpam);
|
|
|
|
|
}
|
|
|
|
|
}
|