Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
363 changes: 363 additions & 0 deletions mail/application_test.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
package mail

import (
"errors"
"os"
"testing"
"time"

"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"

"github.com/goravel/framework/contracts/binding"
contractsconsole "github.com/goravel/framework/contracts/console"
contractsfoundation "github.com/goravel/framework/contracts/foundation"
"github.com/goravel/framework/contracts/mail"
contractsqueue "github.com/goravel/framework/contracts/queue"
"github.com/goravel/framework/foundation/json"
mocksconfig "github.com/goravel/framework/mocks/config"
mocksfoundation "github.com/goravel/framework/mocks/foundation"
mocksmail "github.com/goravel/framework/mocks/mail"
mocksqueue "github.com/goravel/framework/mocks/queue"
"github.com/goravel/framework/queue"
"github.com/goravel/framework/support"
"github.com/goravel/framework/support/color"
Expand Down Expand Up @@ -242,3 +251,357 @@ func (m *TestMailable) Headers() map[string]string {
func (m *TestMailable) Queue() *mail.Queue {
return &mail.Queue{}
}

type stubMailable struct {
attachments []string
content *mail.Content
envelope *mail.Envelope
headers map[string]string
queue *mail.Queue
}

func (s *stubMailable) Attachments() []string { return s.attachments }
func (s *stubMailable) Content() *mail.Content { return s.content }
func (s *stubMailable) Envelope() *mail.Envelope { return s.envelope }
func (s *stubMailable) Headers() map[string]string { return s.headers }
func (s *stubMailable) Queue() *mail.Queue { return s.queue }

func matchWithID(data any) bool {
with, ok := data.(map[string]any)
return ok && with["id"] == 1
}

func TestApplicationBuilderMethodsCloneAndMutate(t *testing.T) {
base := &Application{}

instance := base.To([]string{"to@example.com"}).(*Application)
assert.NotSame(t, base, instance)
assert.Empty(t, base.params.To)
assert.Equal(t, []string{"to@example.com"}, instance.params.To)

contentWith := map[string]any{"name": "goravel"}
chained := instance.
Subject("subject").
Cc([]string{"cc@example.com"}).
Bcc([]string{"bcc@example.com"}).
Attach([]string{"/tmp/a.txt"}).
From(Address("from@example.com", "From Name")).
Headers(map[string]string{"X-Test": "yes"}).
Content(mail.Content{Html: "<h1>Hello</h1>", View: "mail.tmpl", Text: "mail.txt", With: contentWith}).(*Application)

// Builder methods should keep returning the same cloned instance for fluent chaining.
assert.Same(t, instance, chained)
assert.Equal(t, "subject", instance.params.Subject)
assert.Equal(t, []string{"cc@example.com"}, instance.params.CC)
assert.Equal(t, []string{"bcc@example.com"}, instance.params.BCC)
assert.Equal(t, []string{"/tmp/a.txt"}, instance.params.Attachments)
assert.Equal(t, "from@example.com", instance.params.FromAddress)
assert.Equal(t, "From Name", instance.params.FromName)
assert.Equal(t, map[string]string{"X-Test": "yes"}, instance.params.Headers)
assert.Equal(t, "<h1>Hello</h1>", instance.params.HTML)
assert.Equal(t, "mail.tmpl", instance.view)
assert.Equal(t, "mail.txt", instance.text)
assert.Equal(t, contentWith, instance.with)
}

func TestApplicationSetUsingMailable(t *testing.T) {
app := &Application{params: Params{HTML: "old-html", Subject: "old-subject"}}

mailable := &stubMailable{
attachments: []string{"/tmp/logo.png"},
content: &mail.Content{
Html: "new-html",
View: "email.tmpl",
Text: "email.txt",
With: map[string]any{"name": "goravel"},
},
envelope: &mail.Envelope{
From: mail.Address{Address: "from@example.com", Name: "Mailer"},
To: []string{"to@example.com"},
Cc: []string{"cc@example.com"},
Bcc: []string{"bcc@example.com"},
Subject: "new-subject",
},
headers: map[string]string{"X-Test": "true"},
}

app.setUsingMailable(mailable)

assert.Equal(t, "new-html", app.params.HTML)
assert.Equal(t, []string{"/tmp/logo.png"}, app.params.Attachments)
assert.Equal(t, map[string]string{"X-Test": "true"}, app.params.Headers)
assert.Equal(t, "from@example.com", app.params.FromAddress)
assert.Equal(t, "Mailer", app.params.FromName)
assert.Equal(t, []string{"to@example.com"}, app.params.To)
assert.Equal(t, []string{"cc@example.com"}, app.params.CC)
assert.Equal(t, []string{"bcc@example.com"}, app.params.BCC)
assert.Equal(t, "new-subject", app.params.Subject)
assert.Equal(t, "email.tmpl", app.view)
assert.Equal(t, "email.txt", app.text)
assert.Equal(t, map[string]any{"name": "goravel"}, app.with)
}

func TestApplicationRenderViewTemplate(t *testing.T) {
t.Run("success", func(t *testing.T) {
template := mocksmail.NewTemplate(t)
template.EXPECT().Render("mail.tmpl", map[string]any{"id": 1}).Return("<h1>Hello</h1>", nil).Once()
template.EXPECT().Render("mail.txt", map[string]any{"id": 1}).Return("Hello", nil).Once()

app := &Application{template: template, view: "mail.tmpl", text: "mail.txt", with: map[string]any{"id": 1}}

err := app.renderViewTemplate()
assert.NoError(t, err)
assert.Equal(t, "<h1>Hello</h1>", app.params.HTML)
assert.Equal(t, "Hello", app.params.Text)
})

t.Run("html render failed", func(t *testing.T) {
template := mocksmail.NewTemplate(t)
template.EXPECT().Render("mail.tmpl", mock.MatchedBy(matchWithID)).Return("", errors.New("render failed")).Once()

app := &Application{template: template, view: "mail.tmpl", with: map[string]any{"id": 1}}
err := app.renderViewTemplate()
assert.ErrorContains(t, err, "render failed")
})

t.Run("text render failed", func(t *testing.T) {
template := mocksmail.NewTemplate(t)
template.EXPECT().Render("mail.txt", mock.MatchedBy(matchWithID)).Return("", errors.New("text render failed")).Once()

app := &Application{template: template, text: "mail.txt", with: map[string]any{"id": 1}}
err := app.renderViewTemplate()
assert.ErrorContains(t, err, "text render failed")
})
}

func TestApplicationQueue(t *testing.T) {
mockQueue := mocksqueue.NewQueue(t)
pendingJob := mocksqueue.NewPendingJob(t)
mockConfig := mocksconfig.NewConfig(t)

mailable := &stubMailable{
attachments: []string{"/tmp/logo.png"},
content: &mail.Content{Html: "<h1>Queue</h1>"},
envelope: &mail.Envelope{
From: mail.Address{Address: "from@example.com", Name: "From"},
To: []string{"to@example.com"},
Cc: []string{"cc@example.com"},
Bcc: []string{"bcc@example.com"},
Subject: "queue-subject",
},
headers: map[string]string{"X-Test": "queue"},
queue: &mail.Queue{Connection: "redis", Queue: "emails"},
}

mockQueue.EXPECT().Job(
mock.MatchedBy(func(job contractsqueue.Job) bool { return job != nil && job.Signature() == "goravel_send_mail_job" }),
mock.MatchedBy(func(args []contractsqueue.Arg) bool { return len(args) == 10 }),
).
Run(func(job contractsqueue.Job, args ...[]contractsqueue.Arg) {
assert.Equal(t, "goravel_send_mail_job", job.Signature())
assert.Len(t, args, 1)
assert.Len(t, args[0], 10)
assert.Equal(t, "queue-subject", args[0][0].Value)
assert.Equal(t, "<h1>Queue</h1>", args[0][1].Value)
assert.Equal(t, "", args[0][2].Value)
assert.Equal(t, "from@example.com", args[0][3].Value)
assert.Equal(t, "From", args[0][4].Value)
assert.Equal(t, []string{"to@example.com"}, args[0][5].Value)
assert.Equal(t, []string{"cc@example.com"}, args[0][6].Value)
assert.Equal(t, []string{"bcc@example.com"}, args[0][7].Value)
assert.Equal(t, []string{"/tmp/logo.png"}, args[0][8].Value)
assert.Equal(t, []string{"X-Test: queue"}, args[0][9].Value)
}).
Return(pendingJob).Once()
pendingJob.EXPECT().OnConnection("redis").Return(pendingJob).Once()
pendingJob.EXPECT().OnQueue("emails").Return(pendingJob).Once()
pendingJob.EXPECT().Dispatch().Return(nil).Once()

app := &Application{config: mockConfig, queue: mockQueue}

err := app.Queue(mailable)
assert.NoError(t, err)
}

func TestApplicationQueueRenderError(t *testing.T) {
template := mocksmail.NewTemplate(t)
template.EXPECT().Render("mail.tmpl", mock.MatchedBy(matchWithID)).Return("", errors.New("render failed")).Once()

app := &Application{template: template, view: "mail.tmpl", with: map[string]any{"id": 1}}

err := app.Queue()
assert.ErrorContains(t, err, "render failed")
}

func TestApplicationSendRenderError(t *testing.T) {
template := mocksmail.NewTemplate(t)
template.EXPECT().Render("mail.tmpl", mock.MatchedBy(matchWithID)).Return("", errors.New("render failed")).Once()

app := &Application{template: template}
err := app.Send(&stubMailable{content: &mail.Content{View: "mail.tmpl", With: map[string]any{"id": 1}}})

assert.ErrorContains(t, err, "render failed")
}

func TestLoginAuth(t *testing.T) {
auth := LoginAuth("user", "pass")

method, payload, err := auth.Start(nil)
assert.NoError(t, err)
assert.Equal(t, "LOGIN", method)
assert.Equal(t, []byte("user"), payload)

next, err := auth.Next([]byte("Username:"), true)
assert.NoError(t, err)
assert.Equal(t, []byte("user"), next)

next, err = auth.Next([]byte("Password:"), true)
assert.NoError(t, err)
assert.Equal(t, []byte("pass"), next)

next, err = auth.Next([]byte("Unknown:"), true)
assert.NoError(t, err)
assert.Nil(t, next)

next, err = auth.Next([]byte("Username:"), false)
assert.NoError(t, err)
assert.Nil(t, next)
}

func TestNewApplication(t *testing.T) {
t.Run("success", func(t *testing.T) {
mockConfig := mocksconfig.NewConfig(t)
mockConfig.EXPECT().GetString("mail.template.default", "html").Return("mail_unit_success").Once()
mockConfig.EXPECT().GetString("mail.template.engines.mail_unit_success.driver", "html").Return("html").Once()
mockConfig.EXPECT().GetString("mail.template.engines.mail_unit_success.path", "resources/views/mail").Return(".").Once()

app, err := NewApplication(mockConfig, nil)
assert.NoError(t, err)
assert.NotNil(t, app)
assert.Equal(t, mockConfig, app.config)
})

t.Run("unsupported template driver", func(t *testing.T) {
mockConfig := mocksconfig.NewConfig(t)
mockConfig.EXPECT().GetString("mail.template.default", "html").Return("mail_unit_fail").Once()
mockConfig.EXPECT().GetString("mail.template.engines.mail_unit_fail.driver", "html").Return("unsupported").Once()

app, err := NewApplication(mockConfig, nil)
assert.Nil(t, app)
assert.ErrorContains(t, err, "not supported")
})
}

func TestSendMailAttachmentError(t *testing.T) {
mockConfig := mocksconfig.NewConfig(t)
mockConfig.EXPECT().GetString("mail.from.address").Return("from@example.com").Once()
mockConfig.EXPECT().GetString("mail.from.name").Return("From").Once()

err := SendMail(mockConfig, Params{
To: []string{"to@example.com"},
Attachments: []string{"/tmp/does-not-exist.txt"},
})
assert.ErrorIs(t, err, os.ErrNotExist)
}

func TestServiceProviderRelationship(t *testing.T) {
provider := &ServiceProvider{}
relation := provider.Relationship()

assert.Equal(t, 1, len(relation.Bindings))
assert.Equal(t, binding.Mail, relation.Bindings[0])
assert.Equal(t, binding.Bindings[binding.Mail].Dependencies, relation.Dependencies)
assert.Empty(t, relation.ProvideFor)
}

func TestServiceProviderRegister(t *testing.T) {
provider := &ServiceProvider{}
app := mocksfoundation.NewApplication(t)

app.EXPECT().Bind(
binding.Mail,
mock.AnythingOfType("func(foundation.Application) (interface {}, error)"),
).
Run(func(_ any, callback func(contractsfoundation.Application) (any, error)) {
Comment on lines +523 to +525
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot I mean the code below is unnecessary,

mock.MatchedBy(func(callback interface{}) bool {
			_, ok := callback.(func(contractsfoundation.Application) (interface{}, error))
			return ok
		}),

not others like:

mock.MatchedBy(matchWithID)

mock.MatchedBy(func(job contractsqueue.Job) bool { return job != nil && job.Signature() == "goravel_send_mail_job" }),
		mock.MatchedBy(func(args []contractsqueue.Arg) bool { return len(args) == 10 }),
	).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 6fab0aa: restored the mock.MatchedBy(matchWithID) and queue mock.MatchedBy(...) matchers, and kept only the bind-callback simplification. No UI changes (test-only), so there is no screenshot to provide.

t.Run("without config", func(t *testing.T) {
withoutConfig := mocksfoundation.NewApplication(t)
withoutConfig.EXPECT().MakeConfig().Return(nil).Once()

instance, err := callback(withoutConfig)
assert.Nil(t, instance)
assert.Error(t, err)
})

t.Run("without queue", func(t *testing.T) {
withoutQueue := mocksfoundation.NewApplication(t)
configOnly := mocksconfig.NewConfig(t)
withoutQueue.EXPECT().MakeConfig().Return(configOnly).Once()
withoutQueue.EXPECT().MakeQueue().Return(nil).Once()

instance, err := callback(withoutQueue)
assert.Nil(t, instance)
assert.Error(t, err)
})

t.Run("with config and queue", func(t *testing.T) {
withAll := mocksfoundation.NewApplication(t)
configAndQueue := mocksconfig.NewConfig(t)
queue := mocksqueue.NewQueue(t)
withAll.EXPECT().MakeConfig().Return(configAndQueue).Once()
withAll.EXPECT().MakeQueue().Return(queue).Once()
configAndQueue.EXPECT().GetString("mail.template.default", "html").Return("mail_service_provider").Once()
configAndQueue.EXPECT().GetString("mail.template.engines.mail_service_provider.driver", "html").Return("html").Once()
configAndQueue.EXPECT().GetString("mail.template.engines.mail_service_provider.path", "resources/views/mail").Return(".").Once()

instance, err := callback(withAll)
assert.NoError(t, err)
assert.NotNil(t, instance)
})
}).
Once()

provider.Register(app)
}

func TestServiceProviderBootAndRegisterJobs(t *testing.T) {
t.Run("boot registers command and job", func(t *testing.T) {
provider := &ServiceProvider{}
app := mocksfoundation.NewApplication(t)
queue := mocksqueue.NewQueue(t)
config := mocksconfig.NewConfig(t)

app.EXPECT().Commands(mock.AnythingOfType("[]console.Command")).
Run(func(commands []contractsconsole.Command) {
assert.Len(t, commands, 1)
}).
Once()
app.EXPECT().MakeQueue().Return(queue).Once()
app.EXPECT().MakeConfig().Return(config).Once()
queue.EXPECT().Register(mock.AnythingOfType("[]queue.Job")).
Run(func(jobs []contractsqueue.Job) {
assert.Len(t, jobs, 1)
assert.Equal(t, "goravel_send_mail_job", jobs[0].Signature())
}).
Once()

provider.Boot(app)
})

t.Run("registerJobs skips when queue is nil", func(t *testing.T) {
provider := &ServiceProvider{}
app := mocksfoundation.NewApplication(t)
app.EXPECT().MakeQueue().Return(nil).Once()

provider.registerJobs(app)
})

t.Run("registerJobs skips when config is nil", func(t *testing.T) {
provider := &ServiceProvider{}
app := mocksfoundation.NewApplication(t)
queue := mocksqueue.NewQueue(t)
app.EXPECT().MakeQueue().Return(queue).Once()
app.EXPECT().MakeConfig().Return(nil).Once()

provider.registerJobs(app)
})
}
Loading
Loading