211 lines
5.3 KiB
Go
211 lines
5.3 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"internal-mail-skill/internal/db"
|
|
"internal-mail-skill/internal/model"
|
|
mailsmtp "internal-mail-skill/internal/smtp"
|
|
)
|
|
|
|
func TestThreadAssemblyLinksReferences(t *testing.T) {
|
|
ctx := context.Background()
|
|
store, err := db.Open(":memory:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer store.Close()
|
|
if err := store.Migrate(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
svc := New(store, nil, nil, "default")
|
|
|
|
root, err := svc.SaveParsedEmail(ctx, SaveEmailInput{
|
|
POP3UID: "uid-root",
|
|
Parsed: model.ParsedEmail{
|
|
MessageIDHeader: "<root@example.com>",
|
|
Subject: "Root",
|
|
SenderEmail: "a@example.com",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reply, err := svc.SaveParsedEmail(ctx, SaveEmailInput{
|
|
POP3UID: "uid-reply",
|
|
Parsed: model.ParsedEmail{
|
|
MessageIDHeader: "<reply@example.com>",
|
|
InReplyTo: "<root@example.com>",
|
|
References: "<root@example.com>",
|
|
Subject: "Re: Root",
|
|
SenderEmail: "b@example.com",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
thread, err := svc.GetThread(ctx, reply)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(thread) != 2 || thread[0].ID != root || thread[1].ID != reply {
|
|
t.Fatalf("thread = %#v", thread)
|
|
}
|
|
}
|
|
|
|
func TestSearchMarkLabelsAndDraftLifecycle(t *testing.T) {
|
|
ctx := context.Background()
|
|
store, err := db.Open(":memory:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer store.Close()
|
|
if err := store.Migrate(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
svc := New(store, nil, nil, "default")
|
|
|
|
emailID, err := svc.SaveParsedEmail(ctx, SaveEmailInput{
|
|
POP3UID: "uid-1",
|
|
Parsed: model.ParsedEmail{
|
|
MessageIDHeader: "<m@example.com>",
|
|
Subject: "Quarterly Plan",
|
|
BodyText: "Please review",
|
|
SenderEmail: "boss@example.com",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := svc.MarkEmail(ctx, emailID, MarkInput{Read: boolPtr(true), Processed: boolPtr(true)}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
label, err := svc.CreateLabel(ctx, "work", "#336699")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := svc.ApplyLabels(ctx, []int64{emailID}, []int64{label.ID}, "add"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
results, err := svc.SearchEmails(ctx, SearchFilters{Q: "Quarterly", Read: boolPtr(true), Limit: 10})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(results) != 1 || results[0].Labels[0].Name != "work" {
|
|
t.Fatalf("search results = %#v", results)
|
|
}
|
|
|
|
draft, err := svc.CreateDraft(ctx, DraftInput{To: []string{"x@example.com"}, Subject: "Draft", Body: "Hi"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
updated, err := svc.UpdateDraft(ctx, draft.ID, DraftInput{To: []string{"x@example.com"}, Subject: "Updated", Body: "Hello"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if updated.Subject != "Updated" {
|
|
t.Fatalf("updated subject = %q", updated.Subject)
|
|
}
|
|
}
|
|
|
|
func TestSaveParsedEmailRollsBackWhenAttachmentStorageFails(t *testing.T) {
|
|
ctx := context.Background()
|
|
store, err := db.Open(":memory:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer store.Close()
|
|
if err := store.Migrate(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dir := t.TempDir()
|
|
blocker := filepath.Join(dir, "attachments-blocker")
|
|
if err := os.WriteFile(blocker, []byte("not a directory"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
svc := NewWithOptions(store, nil, nil, "default", Options{AttachmentDir: blocker})
|
|
|
|
_, err = svc.SaveParsedEmail(ctx, SaveEmailInput{
|
|
POP3UID: "uid-broken-attachment",
|
|
Parsed: model.ParsedEmail{
|
|
MessageIDHeader: "<broken@example.com>",
|
|
Subject: "Broken Attachment",
|
|
SenderEmail: "a@example.com",
|
|
Attachments: []model.ParsedAttachment{{
|
|
Filename: "note.txt",
|
|
ContentType: "text/plain",
|
|
SizeBytes: 4,
|
|
Data: []byte("nope"),
|
|
}},
|
|
},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("SaveParsedEmail() error = nil, want attachment storage failure")
|
|
}
|
|
|
|
results, err := svc.SearchEmails(ctx, SearchFilters{Q: "Broken Attachment", Limit: 10})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(results) != 0 {
|
|
t.Fatalf("failed save left searchable message: %#v", results)
|
|
}
|
|
}
|
|
|
|
func TestSendDraftIsIdempotentAfterSuccess(t *testing.T) {
|
|
ctx := context.Background()
|
|
store, err := db.Open(":memory:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer store.Close()
|
|
if err := store.Migrate(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mailer := &fakeMailer{}
|
|
svc := NewWithOptions(store, nil, mailer, "default", Options{FromAddress: "sender@example.com"})
|
|
|
|
draft, err := svc.CreateDraft(ctx, DraftInput{To: []string{"x@example.com"}, Subject: "Once", Body: "hello"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
first, err := svc.SendDraft(ctx, draft.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := svc.SendDraft(ctx, draft.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if mailer.calls != 1 {
|
|
t.Fatalf("mailer calls = %d, want 1", mailer.calls)
|
|
}
|
|
if second.SentMessage.ID != first.SentMessage.ID {
|
|
t.Fatalf("second send message id = %d, want %d", second.SentMessage.ID, first.SentMessage.ID)
|
|
}
|
|
}
|
|
|
|
func boolPtr(v bool) *bool { return &v }
|
|
|
|
type fakeMailer struct {
|
|
calls int
|
|
err error
|
|
}
|
|
|
|
func (f *fakeMailer) Send(ctx context.Context, req mailsmtp.SendRequest) (mailsmtp.SendResult, error) {
|
|
f.calls++
|
|
if f.err != nil {
|
|
return mailsmtp.SendResult{}, f.err
|
|
}
|
|
if req.FromAddress == "" {
|
|
return mailsmtp.SendResult{}, errors.New("from required")
|
|
}
|
|
return mailsmtp.SendResult{MessageIDHeader: "<sent@example.com>", SentAt: time.Now().UTC()}, nil
|
|
}
|