100 lines
3.0 KiB
Go
100 lines
3.0 KiB
Go
package usecase
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"lambdamail/protocols/internal/application/port"
|
|
"lambdamail/protocols/internal/domain/valueobject"
|
|
)
|
|
|
|
type capturingBlobStorage struct {
|
|
stored []byte
|
|
}
|
|
|
|
func (c *capturingBlobStorage) Store(_ context.Context, r io.Reader) (port.BlobRef, error) {
|
|
b, err := io.ReadAll(r)
|
|
c.stored = b
|
|
return port.BlobRef{ID: uuid.New(), SizeBytes: int64(len(b))}, err
|
|
}
|
|
|
|
type stubScanner struct {
|
|
result *valueobject.ScanResult
|
|
}
|
|
|
|
func (s *stubScanner) Scan(_ context.Context, _ port.ScanInput) (*valueobject.ScanResult, error) {
|
|
return s.result, nil
|
|
}
|
|
|
|
// The scan verdict is only actionable by the user - and by their Sieve rules -
|
|
// if it reaches the stored message. PLAN.md section 6.1 persists the EML
|
|
// together with the verdicts, so the spam headers the scanner produced must be
|
|
// present in the delivered message.
|
|
func TestUseCase_Handle_WritesScanHeadersIntoStoredMessage(t *testing.T) {
|
|
blobs := &capturingBlobStorage{}
|
|
scanner := &stubScanner{result: &valueobject.ScanResult{
|
|
Verdict: valueobject.ScanVerdictSpamJunk,
|
|
Score: 9.5,
|
|
HeadersToAdd: map[string]string{
|
|
"X-Spam-Flag": "YES",
|
|
"X-Spam-Score": "9.50",
|
|
},
|
|
}}
|
|
|
|
uc := NewProcessInboundEmailUseCase(nil, blobs, &recordingMessageRepository{})
|
|
uc.SetScanner(scanner)
|
|
|
|
err := uc.Handle(context.Background(), ProcessInboundEmailInput{
|
|
Sender: "spammer@example.test",
|
|
Recipients: []port.MailboxRecord{{ID: uuid.New()}},
|
|
RecipientAddresses: []string{"victim@example.test"},
|
|
Body: bytes.NewBufferString("Subject: buy now\r\n\r\nbody"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Handle: %v", err)
|
|
}
|
|
|
|
stored := string(blobs.stored)
|
|
for _, want := range []string{"X-Spam-Flag: YES", "X-Spam-Score: 9.50"} {
|
|
if !strings.Contains(stored, want) {
|
|
t.Errorf("stored message is missing %q:\n%s", want, stored)
|
|
}
|
|
}
|
|
if !strings.Contains(stored, "Subject: buy now") {
|
|
t.Errorf("original headers were lost:\n%s", stored)
|
|
}
|
|
// Added headers must precede the original ones and stay inside the header
|
|
// block, i.e. before the first empty line.
|
|
headerBlock, _, found := strings.Cut(stored, "\r\n\r\n")
|
|
if !found {
|
|
t.Fatalf("stored message has no header/body separator:\n%s", stored)
|
|
}
|
|
if !strings.Contains(headerBlock, "X-Spam-Flag: YES") {
|
|
t.Errorf("scan headers leaked into the body:\n%s", stored)
|
|
}
|
|
}
|
|
|
|
func TestUseCase_Handle_LeavesMessageUntouchedWithoutScanHeaders(t *testing.T) {
|
|
blobs := &capturingBlobStorage{}
|
|
uc := NewProcessInboundEmailUseCase(nil, blobs, &recordingMessageRepository{})
|
|
|
|
original := "Subject: hi\r\n\r\nbody"
|
|
err := uc.Handle(context.Background(), ProcessInboundEmailInput{
|
|
Recipients: []port.MailboxRecord{{ID: uuid.New()}},
|
|
RecipientAddresses: []string{"a@example.test"},
|
|
Body: bytes.NewBufferString(original),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Handle: %v", err)
|
|
}
|
|
|
|
if string(blobs.stored) != original {
|
|
t.Errorf("message was rewritten without a scanner: got %q, want %q", blobs.stored, original)
|
|
}
|
|
}
|