Files
LambdaMail/services/protocols/internal/application/usecase/vacation_suppression_test.go
T
Lucas RayanandClaude Opus 5 e4491479ef fix(vacation): descodifica o assunto e responde a cada pessoa uma vez
O assunto que chegou a uma caixa real: "Fora do escritorio: Re:
=?UTF-8?Q?Re=3A_Fora_do_escrit=C3=B3rio?=". Duas coisas erradas, e a
segunda maior do que parece.

Um assunto com acento viaja como palavra codificada (RFC 2047). Colei o
cabecalho em bruto, portanto aquilo foi lido tal e qual por uma pessoa.
E foi tambem o que derrotou a verificacao do "Re:" ja existente:
codificado, todo o assunto comeca por "=?", nunca por "Re:", e por isso
foi acrescentado outro por cima. Descodificar corrige os dois sintomas
de uma vez. Os prefixos acumulados ("Re: RE: Re:") passam a ser
reduzidos a um.

O que estava por tras: aquele assunto era "Re: Fora do escritorio" -
alguem tinha respondido a propria mensagem automatica, e ela respondeu
de novo. Faltava a supressao por remetente. O RFC 5230 seccao 4.1 pede
exactamente isto e propoe sete dias: sem ela, uma conversa de cinco
mensagens produzia cinco copias do mesmo aviso, e quem respondesse ao
aviso recebia outro - que e como uma pessoa e um respondedor automatico
acabam a falar um para o outro indefinidamente.

A supressao falha aberta: um registo que nao se consegue ler deixa a
resposta sair. Ser avisado duas vezes e uma falha menor do que nunca
saber que a caixa esta ausente, que e o proposito da funcionalidade. O
envio so e registado depois de a resposta estar mesmo na fila, para que
um envio falhado nao silencie a mensagem seguinte da mesma pessoa.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 15:15:58 -03:00

140 lines
4.8 KiB
Go

package usecase
import (
"context"
"testing"
"time"
"github.com/google/uuid"
)
// How often the same person may be told you are away.
//
// Without this every message produced a reply, so a conversation of five mails
// produced five identical out-of-office notes - and someone who replied to the
// automatic message itself got another one, which is how a person and an
// autoresponder end up talking past each other indefinitely. RFC 5230 section
// 4.1 calls for a suppression period, and names seven days as the default.
type stubVacationLog struct {
last map[string]time.Time
saved []string
err error
}
func (s *stubVacationLog) LastRepliedAt(_ context.Context, mailbox uuid.UUID, to string) (time.Time, error) {
if s.err != nil {
return time.Time{}, s.err
}
return s.last[to], nil
}
func (s *stubVacationLog) RecordReply(_ context.Context, mailbox uuid.UUID, to string) error {
s.saved = append(s.saved, to)
return nil
}
func newSuppression(log *stubVacationLog) *VacationSuppression {
return NewVacationSuppression(log, 7*24*time.Hour)
}
func TestFirstMessageFromSomeoneGetsAReply(t *testing.T) {
log := &stubVacationLog{last: map[string]time.Time{}}
if !newSuppression(log).ShouldReply(context.Background(), uuid.New(), "amigo@example.test") {
t.Error("the first message from someone got no reply")
}
}
// The second message in the same conversation must not produce a second copy
// of the same notice.
func TestSecondMessageWithinThePeriodIsSilent(t *testing.T) {
log := &stubVacationLog{last: map[string]time.Time{
"amigo@example.test": time.Now().Add(-2 * time.Hour),
}}
if newSuppression(log).ShouldReply(context.Background(), uuid.New(), "amigo@example.test") {
t.Error("replied twice to the same person within the period")
}
}
// Once the period is up, someone writing again is told again - they may not
// remember, and the absence may have changed.
func TestSomeoneWritingAfterThePeriodIsToldAgain(t *testing.T) {
log := &stubVacationLog{last: map[string]time.Time{
"amigo@example.test": time.Now().Add(-8 * 24 * time.Hour),
}}
if !newSuppression(log).ShouldReply(context.Background(), uuid.New(), "amigo@example.test") {
t.Error("stayed silent after the suppression period had passed")
}
}
func TestDifferentPeopleEachGetOne(t *testing.T) {
log := &stubVacationLog{last: map[string]time.Time{
"amigo@example.test": time.Now(),
}}
if !newSuppression(log).ShouldReply(context.Background(), uuid.New(), "outro@example.test") {
t.Error("someone else's message was suppressed")
}
}
// The same person, spelled differently, is the same person.
func TestAddressComparisonIgnoresCase(t *testing.T) {
log := &stubVacationLog{last: map[string]time.Time{
"amigo@example.test": time.Now(),
}}
if newSuppression(log).ShouldReply(context.Background(), uuid.New(), "AMIGO@EXAMPLE.TEST") {
t.Error("the same address in another case got a second reply")
}
}
// A log that cannot be read must not stop the reply: being told twice is a
// smaller failure than never being told at all.
func TestAnUnreadableLogStillReplies(t *testing.T) {
log := &stubVacationLog{err: errTestLogUnavailable}
if !newSuppression(log).ShouldReply(context.Background(), uuid.New(), "amigo@example.test") {
t.Error("an unreadable log silenced the responder")
}
}
// Without a log configured at all, the responder behaves as it did before.
func TestNoLogMeansNoSuppression(t *testing.T) {
if !NewVacationSuppression(nil, time.Hour).ShouldReply(context.Background(), uuid.New(), "a@b.test") {
t.Error("a missing log silenced the responder")
}
}
func TestReplyIsRecorded(t *testing.T) {
log := &stubVacationLog{last: map[string]time.Time{}}
s := newSuppression(log)
s.Record(context.Background(), uuid.New(), "Amigo@Example.test")
if len(log.saved) != 1 || log.saved[0] != "amigo@example.test" {
t.Errorf("recorded %v, want the address lowercased", log.saved)
}
}
var errTestLogUnavailable = errTestUnavailable{}
type errTestUnavailable struct{}
func (errTestUnavailable) Error() string { return "log unavailable" }
// --- through the delivery path -------------------------------------------
// The second message from the same person in the same week produces no second
// notice, which is the behaviour the reader actually noticed was missing.
func TestDeliveryTellsEachSenderOnlyOnce(t *testing.T) {
replied := map[string]time.Time{}
suppression := NewVacationSuppression(&stubVacationLog{last: replied}, 7*24*time.Hour)
mailboxID := uuid.New()
if !suppression.ShouldReply(context.Background(), mailboxID, "amigo@example.test") {
t.Fatal("the first message got no reply")
}
// Recording is what the delivery path does once the reply is queued.
replied["amigo@example.test"] = time.Now()
if suppression.ShouldReply(context.Background(), mailboxID, "amigo@example.test") {
t.Error("a second message in the same week produced a second notice")
}
}