Files
LambdaMail/services/protocols/internal/presentation/imap/search_test.go
T
Lucas RayanandClaude Sonnet 5 0ec0698079 #5: F2 sub-project 2 - IMAP SEARCH, EXPUNGE, COPY, MOVE
Adds the second F2 sub-project on top of the merged IMAP core: full
SEARCH criteria matching (flags, dates, size, headers, body/text,
NOT/OR), EXPUNGE (soft-delete via expunged_at), COPY, and MOVE (RFC
6851, advertised via CapMove/CapUIDPlus). Verified end-to-end with a
real go-imap/v2 client against embedded Postgres.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 09:40:47 -03:00

134 lines
5.0 KiB
Go

package imappresentation
import (
"testing"
"time"
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/imapserver"
"github.com/google/uuid"
"lambdamail/protocols/internal/application/port"
)
func TestSession_Search_MatchesByFlag(t *testing.T) {
mailboxID := uuid.New()
auth := &fakeAuthRepository{byAddress: map[string]port.MailboxAuth{
"user@example.test": {ID: mailboxID, PasswordHash: testPasswordHash},
}}
folders := &fakeImapFolderRepository{byName: map[string]port.ImapFolderRecord{
"INBOX": {ID: "folder-1"},
}}
messages := &fakeMessageQueryRepository{messages: []port.MessageRecord{
{UID: 1, ReceivedAt: time.Now(), Flags: []string{"\\Seen"}},
{UID: 2, ReceivedAt: time.Now(), Flags: nil},
{UID: 3, ReceivedAt: time.Now(), Flags: []string{"\\Seen", "\\Flagged"}},
}}
sess := newTestSession(auth, folders, messages, &fakeFlagRepository{}, &fakeBlobReader{})
_ = sess.Login("user@example.test", testPassword)
if _, err := sess.Select("INBOX", &imap.SelectOptions{}); err != nil {
t.Fatalf("Select: %v", err)
}
data, err := sess.Search(imapserver.NumKindUID, &imap.SearchCriteria{Flag: []imap.Flag{imap.FlagSeen}}, &imap.SearchOptions{})
if err != nil {
t.Fatalf("Search: %v", err)
}
uidSet, ok := data.All.(imap.UIDSet)
if !ok {
t.Fatalf("data.All is %T, want imap.UIDSet", data.All)
}
nums, ok := uidSet.Nums()
if !ok || len(nums) != 2 || nums[0] != 1 || nums[1] != 3 {
t.Errorf("matched UIDs = %v (ok=%v), want [1 3]", nums, ok)
}
}
func TestSession_Search_MatchesByUIDSetIntersection(t *testing.T) {
mailboxID := uuid.New()
auth := &fakeAuthRepository{byAddress: map[string]port.MailboxAuth{
"user@example.test": {ID: mailboxID, PasswordHash: testPasswordHash},
}}
folders := &fakeImapFolderRepository{byName: map[string]port.ImapFolderRecord{"INBOX": {ID: "folder-1"}}}
messages := &fakeMessageQueryRepository{messages: []port.MessageRecord{
{UID: 1, ReceivedAt: time.Now()},
{UID: 2, ReceivedAt: time.Now()},
{UID: 3, ReceivedAt: time.Now()},
}}
sess := newTestSession(auth, folders, messages, &fakeFlagRepository{}, &fakeBlobReader{})
_ = sess.Login("user@example.test", testPassword)
_, _ = sess.Select("INBOX", &imap.SelectOptions{})
var uidFilter imap.UIDSet
uidFilter.AddRange(2, 0) // "2:*"
data, err := sess.Search(imapserver.NumKindUID, &imap.SearchCriteria{UID: []imap.UIDSet{uidFilter}}, &imap.SearchOptions{})
if err != nil {
t.Fatalf("Search: %v", err)
}
uidSet := data.All.(imap.UIDSet)
nums, _ := uidSet.Nums()
if len(nums) != 2 || nums[0] != 2 || nums[1] != 3 {
t.Errorf("matched UIDs = %v, want [2 3]", nums)
}
}
func TestSession_Search_MatchesByHeaderField(t *testing.T) {
mailboxID := uuid.New()
auth := &fakeAuthRepository{byAddress: map[string]port.MailboxAuth{
"user@example.test": {ID: mailboxID, PasswordHash: testPasswordHash},
}}
folders := &fakeImapFolderRepository{byName: map[string]port.ImapFolderRecord{"INBOX": {ID: "folder-1"}}}
blobA := uuid.New()
blobB := uuid.New()
messages := &fakeMessageQueryRepository{messages: []port.MessageRecord{
{UID: 1, BlobID: blobA, ReceivedAt: time.Now()},
{UID: 2, BlobID: blobB, ReceivedAt: time.Now()},
}}
blobs := &fakeBlobReader{content: map[uuid.UUID][]byte{
blobA: []byte("Subject: hello world\r\n\r\nbody"),
blobB: []byte("Subject: something else\r\n\r\nbody"),
}}
sess := newTestSession(auth, folders, messages, &fakeFlagRepository{}, blobs)
_ = sess.Login("user@example.test", testPassword)
_, _ = sess.Select("INBOX", &imap.SelectOptions{})
data, err := sess.Search(imapserver.NumKindUID, &imap.SearchCriteria{
Header: []imap.SearchCriteriaHeaderField{{Key: "Subject", Value: "hello"}},
}, &imap.SearchOptions{})
if err != nil {
t.Fatalf("Search: %v", err)
}
uidSet := data.All.(imap.UIDSet)
nums, _ := uidSet.Nums()
if len(nums) != 1 || nums[0] != 1 {
t.Errorf("matched UIDs = %v, want [1] (only the message with \"hello\" in Subject)", nums)
}
}
func TestSession_Search_NotCriteriaExcludesMatches(t *testing.T) {
mailboxID := uuid.New()
auth := &fakeAuthRepository{byAddress: map[string]port.MailboxAuth{
"user@example.test": {ID: mailboxID, PasswordHash: testPasswordHash},
}}
folders := &fakeImapFolderRepository{byName: map[string]port.ImapFolderRecord{"INBOX": {ID: "folder-1"}}}
messages := &fakeMessageQueryRepository{messages: []port.MessageRecord{
{UID: 1, ReceivedAt: time.Now(), Flags: []string{"\\Seen"}},
{UID: 2, ReceivedAt: time.Now(), Flags: nil},
}}
sess := newTestSession(auth, folders, messages, &fakeFlagRepository{}, &fakeBlobReader{})
_ = sess.Login("user@example.test", testPassword)
_, _ = sess.Select("INBOX", &imap.SelectOptions{})
data, err := sess.Search(imapserver.NumKindUID, &imap.SearchCriteria{
Not: []imap.SearchCriteria{{Flag: []imap.Flag{imap.FlagSeen}}},
}, &imap.SearchOptions{})
if err != nil {
t.Fatalf("Search: %v", err)
}
uidSet := data.All.(imap.UIDSet)
nums, _ := uidSet.Nums()
if len(nums) != 1 || nums[0] != 2 {
t.Errorf("matched UIDs = %v, want [2] (NOT SEEN excludes the \\Seen message)", nums)
}
}