Skip to content
Open
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
1 change: 0 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,3 @@ GEMINI_AUDIO_MODEL=gemini-3.1-flash-tts-preview
GEMINI_TRANSCRIPTION_MODEL=gemini-3.1-flash-lite

APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_FROM_ADDRESS=
MAIL_FROM_NAME=

SLACK_BOT_TOKEN=

OPENAI_API_KEY=
OPENAI_BASE_URL=
OPENAI_TEXT_MODEL=
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/test-features.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ env:
MAIL_FROM_ADDRESS: ${{ secrets.MAIL_FROM_ADDRESS }}
MAIL_FROM_NAME: ${{ secrets.MAIL_FROM_NAME }}
MAIL_TO: ${{ secrets.MAIL_TO }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
jobs:
ubuntu:
strategy:
Expand Down
2 changes: 2 additions & 0 deletions bootstrap/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
"github.com/goravel/postgres"
"github.com/goravel/redis"
"github.com/goravel/s3"
"github.com/goravel/slack"
"github.com/goravel/sqlite"
"github.com/goravel/sqlserver"
"goravel/packages/viewtest"
Expand Down Expand Up @@ -85,5 +86,6 @@ func Providers() []foundation.ServiceProvider {
&gemini.ServiceProvider{},
&viewtest.ServiceProvider{},
&RouteProvider{},
&slack.ServiceProvider{},
}
}
16 changes: 16 additions & 0 deletions config/slack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package config

import (
"goravel/app/facades"
)

func init() {
config := facades.Config()

config.Add("slack", map[string]any{
// token is the Slack bot token (xoxb-...). Create one at
// https://api.slack.com/apps — needs the chat:write scope, and
// the bot must be invited to any channel it should post in.
"token": config.Env("SLACK_BOT_TOKEN", ""),
})
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ require (
github.com/goravel/postgres v1.18.0
github.com/goravel/redis v1.18.1-0.20260804080002-ea3e2dcfbdf6
github.com/goravel/s3 v1.18.0
github.com/goravel/slack v0.0.0-20260819081718-3f42493b34e4
github.com/goravel/sqlite v1.18.0
github.com/goravel/sqlserver v1.18.0
github.com/gorilla/websocket v1.5.3
Expand Down Expand Up @@ -184,6 +185,7 @@ require (
github.com/samber/lo v1.53.0 // indirect
github.com/samber/slog-common v0.21.0 // indirect
github.com/samber/slog-multi v1.8.0 // indirect
github.com/slack-go/slack v0.17.3 // indirect
github.com/sosodev/duration v1.3.1 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI=
github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
Expand Down Expand Up @@ -316,6 +318,8 @@ github.com/goravel/redis v1.18.1-0.20260804080002-ea3e2dcfbdf6 h1:XGd6HrfS9gyi1i
github.com/goravel/redis v1.18.1-0.20260804080002-ea3e2dcfbdf6/go.mod h1:2Y6o5g5Pl1tXx6qPf0YAg6r5pXmSSPqvAlJ51GrdZCQ=
github.com/goravel/s3 v1.18.0 h1:kdq9jhy0MeWEdpYMk2cGDBjGOr6z/yW3/r8RrdWMagg=
github.com/goravel/s3 v1.18.0/go.mod h1:2qntIhQQA1/9678PTlb8rWOoGcLTjXG6/hQ8fCayqi4=
github.com/goravel/slack v0.0.0-20260819081718-3f42493b34e4 h1:IRqDXV89klaNQWSEDWngN70Icoh/uEF4MnYYItgNbaA=
github.com/goravel/slack v0.0.0-20260819081718-3f42493b34e4/go.mod h1:sKIbrIzt8FRPLUgDIXgdzi4Q4K34XP4czWjiw8WETb8=
github.com/goravel/sqlite v1.18.0 h1:d9iZHQ9siZXwgOV9Q8tyGr4fbT0Q6LljX/7XcX7fibY=
github.com/goravel/sqlite v1.18.0/go.mod h1:uciL6tYnM7FDJagBWQlFzkSPtcaWewxpQvJCmBZmKVg=
github.com/goravel/sqlserver v1.18.0 h1:9ZoSZbZUIzpTuvlarWk54d0IjMneugsHNxxYxvtnf4Y=
Expand Down Expand Up @@ -498,6 +502,8 @@ github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/shamaton/msgpack/v3 v3.1.2 h1:d5gWAIyMU4M0WgDjz6IFSCuXJUA2dFwRHBpDclE8CLw=
github.com/shamaton/msgpack/v3 v3.1.2/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc=
github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g=
github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk=
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
Expand Down
12 changes: 12 additions & 0 deletions package_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,15 @@ func TestInstallAndPublishAndUninstallLocalPackage(t *testing.T) {
assert.False(t, file.Contain(path.Bootstrap("providers.go"), "goravel/packages/sms"))
assert.False(t, file.Contain(path.Bootstrap("providers.go"), "&sms.ServiceProvider{}"))
}

func TestInstallAndUninstallSlack(t *testing.T) {
assert.NoError(t, facades.Artisan().Call("package:uninstall github.com/goravel/slack@master"))
assert.False(t, file.Contain(path.Bootstrap("providers.go"), "&slack.ServiceProvider{},"))
assert.False(t, file.Contain(path.Bootstrap("providers.go"), "github.com/goravel/slack"))
assert.False(t, file.Exists(path.Config("slack.go")))

assert.NoError(t, facades.Artisan().Call("package:install github.com/goravel/slack@master"))
assert.True(t, file.Contain(path.Bootstrap("providers.go"), "&slack.ServiceProvider{},"))
assert.True(t, file.Contain(path.Bootstrap("providers.go"), "github.com/goravel/slack"))
assert.True(t, file.Exists(path.Config("slack.go")))
}
9 changes: 5 additions & 4 deletions tests/feature/notification_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/goravel/framework/support/file"
"github.com/goravel/framework/support/path"
"github.com/goravel/framework/support/str"
slackcontracts "github.com/goravel/slack/contracts"

"goravel/app/facades"
"goravel/app/models"
Expand Down Expand Up @@ -57,7 +58,7 @@ func (s *NotificationTestSuite) TestFacadeResolves() {
s.NotNil(facades.Notification())
s.NotNil(facades.Notification().Channel(notification.ChannelDatabase))
s.NotNil(facades.Notification().Channel(notification.ChannelMail))
s.Nil(facades.Notification().Channel("slack"))
s.NotNil(facades.Notification().Channel(slackcontracts.ChannelName))
}

func (s *NotificationTestSuite) TestSendDatabaseNotification() {
Expand Down Expand Up @@ -310,9 +311,9 @@ func (s *NotificationTestSuite) TestCustomChannel() {
}

func (s *NotificationTestSuite) TestSendToUnknownChannel() {
err := facades.Notification().Route("slack", "route").NotifyNow(&unknownChannelNotification{})
err := facades.Notification().Route("unknown-channel", "route").NotifyNow(&unknownChannelNotification{})
s.Error(err)
s.ErrorIs(err, frameworkerrors.NotificationChannelNotFound.Args("slack"))
s.ErrorIs(err, frameworkerrors.NotificationChannelNotFound.Args("unknown-channel"))
}

func (s *NotificationTestSuite) TestSendMailNotification() {
Expand Down Expand Up @@ -1016,7 +1017,7 @@ func (r *chainedViaNotification) ToDatabase(notifiable notification.Notifiable)
type unknownChannelNotification struct{}

func (r *unknownChannelNotification) Via(notifiable notification.Notifiable) []string {
return []string{"slack"}
return []string{"unknown-channel"}
}

// mailRoutableNotifiable implements MailRoutable with a fixed recipient.
Expand Down
88 changes: 88 additions & 0 deletions tests/feature/slack_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package feature

import (
"testing"

"github.com/goravel/framework/contracts/notification"
slackcontracts "github.com/goravel/slack/contracts"
"github.com/stretchr/testify/suite"

"goravel/app/facades"
"goravel/tests"
)

type SlackTestSuite struct {
suite.Suite
tests.TestCase
}

func TestSlackTestSuite(t *testing.T) {
suite.Run(t, &SlackTestSuite{})
}

// requireSlack skips the test unless a bot token is configured, and returns
// the channel to post to. Mirrors the mail live-test pattern: skipped locally,
// exercised for real in CI when secrets are set.
func (s *SlackTestSuite) requireSlack() string {
if facades.Config().GetString("slack.token") == "" {
s.T().Skip("skipping Slack test: set SLACK_BOT_TOKEN to run against a real Slack workspace")
}
return "general"
}

func (s *SlackTestSuite) TestSlackChannelRegistered() {
s.NotNil(facades.Notification().Channel(slackcontracts.ChannelName))
}

func (s *SlackTestSuite) TestSendSlackTextNotification() {
channel := s.requireSlack()

err := facades.Notification().
Route(slackcontracts.ChannelName, channel).
NotifyNow(&slackTextNotification{})
s.NoErrorf(err, "sending to channel %q", channel)
}

func (s *SlackTestSuite) TestSendSlackRichNotification() {
channel := s.requireSlack()

n := &slackRichNotification{msg: slackcontracts.Message{
Text: "Goravel Slack integration test — rich delivery",
Attachments: []slackcontracts.Attachment{
{
Title: "Details", Text: "Invoice #123", Color: "good",
Fields: []slackcontracts.Field{
{Title: "Amount", Value: "$99.00", Short: true},
{Title: "Customer", Value: "Acme Corp", Short: true},
},
},
},
}}

err := facades.Notification().
Route(slackcontracts.ChannelName, channel).
NotifyNow(n)
s.NoErrorf(err, "sending to channel %q", channel)
}

type slackTextNotification struct{}

func (r *slackTextNotification) Via(_ notification.Notifiable) []string {
return []string{slackcontracts.ChannelName}
}

func (r *slackTextNotification) ToSlack(_ notification.Notifiable) slackcontracts.Message {
return slackcontracts.Message{Text: "Goravel Slack integration test — text delivery"}
}

type slackRichNotification struct {
msg slackcontracts.Message
}

func (r *slackRichNotification) Via(_ notification.Notifiable) []string {
return []string{slackcontracts.ChannelName}
}

func (r *slackRichNotification) ToSlack(_ notification.Notifiable) slackcontracts.Message {
return r.msg
}
Loading