diff --git a/.gitignore b/.gitignore index 75bd3258..046c23f6 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ tmp/ .gh-docker-token .env coverage.out +cover.html diff --git a/Makefile b/Makefile index af2aff67..37d86cfc 100644 --- a/Makefile +++ b/Makefile @@ -18,14 +18,21 @@ setup: # TESTS ############################################################################### -# Run all the tests test: - go test -failfast -race -timeout=5m ./... + go test -failfast -race ./... .PHONY: test -cover: - go test -race -covermode=atomic -coverprofile=coverage.out ./... -.PHONY: cover +gen-coverage: + @go test -race -covermode=atomic -coverprofile=coverage.out ./... > /dev/null +.PHONY: gen-coverage + +coverage: gen-coverage + go tool cover -func coverage.out +.PHONY: coverage + +coverage-html: gen-coverage + go tool cover -html=coverage.out -o cover.html +.PHONY: coverage-html mock: go generate ./... diff --git a/README.md b/README.md index 628349cc..515040eb 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,7 @@ Yes, please! Contributions of all kinds are very welcome! Feel free to check our | [Discord](https://discord.com) | [service/discord](service/discord) | [bwmarrin/discordgo](https://github.com/bwmarrin/discordgo) | :heavy_check_mark: | | [Email](https://wikipedia.org/wiki/Email) | [service/mail](service/mail) | [jordan-wright/email](https://github.com/jordan-wright/email) | :heavy_check_mark: | | [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging) | [service/fcm](service/fcm) | [appleboy/go-fcm](https://github.com/appleboy/go-fcm) | :heavy_check_mark: | +| [HTTP](https://wikipedia.org/wiki/Hypertext_Transfer_Protocol) | [service/http](service/http) | - | :heavy_check_mark: | | [Lark](https://www.larksuite.com/) | [service/lark](service/lark) | [go-lark/lark](https://github.com/go-lark/lark) | :heavy_check_mark: | | [Line](https://line.me) | [service/line](service/line) | [line/line-bot-sdk-go](https://github.com/line/line-bot-sdk-go) | :heavy_check_mark: | | [Line Notify](https://notify-bot.line.me) | [service/line](service/line) | [utahta/go-linenotify](https://github.com/utahta/go-linenotify) | :heavy_check_mark: | diff --git a/go.mod b/go.mod index 03e20b57..54e77247 100644 --- a/go.mod +++ b/go.mod @@ -76,6 +76,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.8.1 // indirect github.com/sendgrid/rest v2.6.9+incompatible // indirect github.com/sirupsen/logrus v1.9.0 // indirect github.com/spf13/cast v1.5.0 // indirect @@ -94,5 +95,6 @@ require ( golang.org/x/text v0.6.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.1 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index cd7ac3fc..96f73462 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,15 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/Jeffail/gabs v1.4.0 h1://5fYRRTq1edjfIrQGvdkcd22pkYUrHZ5YC/H2GJVAo= github.com/Jeffail/gabs v1.4.0/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc= +github.com/Rhymen/go-whatsapp v0.0.0/go.mod h1:rdQr95g2C1xcOfM7QGOhza58HeI3I+tZ/bbluv7VazA= +github.com/Rhymen/go-whatsapp v0.1.1 h1:OK+bCugQcr2YjyYKeDzULqCtM50TPUFM6LvQtszKfcw= +github.com/Rhymen/go-whatsapp v0.1.1/go.mod h1:o7jjkvKnigfu432dMbQ/w4PH0Yp5u4Y6ysCNjUlcYCk= +github.com/Rhymen/go-whatsapp/examples/echo v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:zgCiQtBtZ4P4gFWvwl9aashsdwOcbb/EHOGRmSzM8ME= +github.com/Rhymen/go-whatsapp/examples/restoreSession v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:5sCUSpG616ZoSJhlt9iBNI/KXBqrVLcNUJqg7J9+8pU= +github.com/Rhymen/go-whatsapp/examples/sendImage v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:RdiyhanVEGXTam+mZ3k6Y3VDCCvXYCwReOoxGozqhHw= +github.com/Rhymen/go-whatsapp/examples/sendTextMessages v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:suwzklatySS3Q0+NCxCDh5hYfgXdQUWU1DNcxwAxStM= +github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20220903135808-56c5346a1a28 h1:OJe0G++TYGhE525XnkrF9KF15D1WtQdyrk19SFwRrKk= +github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20220903135808-56c5346a1a28/go.mod h1:rjP7sIipbZcagro/6TCk6X0ZeFT2eyudH5+fve/cbBA= github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20221121042443-a3fd332d56d9 h1:vuu1KBsr6l7XU3CHsWESP/4B1SNd+VZkrgeFZsUXrsY= github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20221121042443-a3fd332d56d9/go.mod h1:rjP7sIipbZcagro/6TCk6X0ZeFT2eyudH5+fve/cbBA= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= @@ -9,6 +18,34 @@ github.com/appleboy/go-fcm v0.1.5 h1:fKbcZf/7vwGsvDkcop8a+kCHnK+tt4wXX0X7uEzwI6E github.com/appleboy/go-fcm v0.1.5/go.mod h1:MSxZ4LqGRsnywOjnlXJXMqbjZrG4vf+0oHitfC9HRH0= github.com/atc0005/go-teams-notify/v2 v2.6.1 h1:t22ybzQuaQs4UJe4ceF5VYGsPhs6ir3nZOId/FBy6Go= github.com/atc0005/go-teams-notify/v2 v2.6.1/go.mod h1:xo6GejLDHn3tWBA181F8LrllIL0xC1uRsRxq7YNXaaY= +github.com/aws/aws-sdk-go-v2 v1.16.14 h1:db6GvO4Z2UqHt5gvT0lr6J5x5P+oQ7bdRzczVaRekMU= +github.com/aws/aws-sdk-go-v2 v1.16.14/go.mod h1:s/G+UV29dECbF5rf+RNj1xhlmvoNurGSr+McVSRj59w= +github.com/aws/aws-sdk-go-v2/config v1.17.5 h1:+NS1BWvprx7nHcIk5o32LrZgifs/7Pm1V2nWjQgZ2H0= +github.com/aws/aws-sdk-go-v2/config v1.17.5/go.mod h1:H0cvPNDO3uExWts/9PDhD/0ne2esu1uaIulwn1vkwxM= +github.com/aws/aws-sdk-go-v2/credentials v1.12.18 h1:HF62tbhARhgLfvmfwUbL9qZ+dkbZYzbFdxBb3l5gr7Q= +github.com/aws/aws-sdk-go-v2/credentials v1.12.18/go.mod h1:O7n/CPagQ33rfG6h7vR/W02ammuc5CrsSM22cNZp9so= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.15 h1:nkQ+aI0OCeYfzrBipL6ja/6VEbUnHQoZHBHtoK+Nzxw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.15/go.mod h1:Oz2/qWINxIgSmoZT9adpxJy2UhpcOAI3TIyWgYMVSz0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.21 h1:gRIXnmAVNyoRQywdNtpAkgY+f30QNzgF53Q5OobNZZs= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.21/go.mod h1:XsmHMV9c512xgsW01q7H0ut+UQQQpWX8QsFbdLHDwaU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.15 h1:noAhOo2mMDyYhTx99aYPvQw16T3fQ/DiKAv9fzpIKH8= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.15/go.mod h1:kjJ4CyD9M3Wq88GYg3IPfj67Rs0Uvz8aXK7MJ8BvE4I= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.22 h1:nF+E8HfYpOMw6M5oA9efB602VC00IHNQnB5CmFvZPvA= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.22/go.mod h1:tltHVGy977LrSOgRR5aV9+miyno/Gul/uJNPKS7FzP4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.15 h1:xlf0J6DUgAj/ocvKQxCmad8Bu1lJuRbt5Wu+4G1xw1g= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.15/go.mod h1:ZVJ7ejRl4+tkWMuCwjXoy0jd8fF5u3RCyWjSVjUIvQE= +github.com/aws/aws-sdk-go-v2/service/ses v1.14.16 h1:F+1UqtImFZoJs48f4DO/usm8P+Ok5i6Ll5+T9csXXvU= +github.com/aws/aws-sdk-go-v2/service/ses v1.14.16/go.mod h1:ufZIF0CTaAcA/Yammf5sQSGut2kLgXEOY5rssBpi9eE= +github.com/aws/aws-sdk-go-v2/service/sns v1.17.17 h1:VKMhV1kisP1oNtCZQ2b9Aj8Hx1vwCC/bLlg2rw4tW/0= +github.com/aws/aws-sdk-go-v2/service/sns v1.17.17/go.mod h1:hygPv9etah0QZWMe7TEE+PCPe1VL+1tfwYvJZz478uc= +github.com/aws/aws-sdk-go-v2/service/sso v1.11.21 h1:7jUFr+7F4MzIjCZzy7ygRtXFQcQ0kAbT0gUvtUeAdyU= +github.com/aws/aws-sdk-go-v2/service/sso v1.11.21/go.mod h1:q8nYq51W3gpZempYsAD83fPRlrOTMCwN+Ahg4BKFTXQ= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.3 h1:UTTPNP3/WzZa7hoHP3Szb/Yl0bM3NoBrf5ABy1OArUM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.3/go.mod h1:+IF75RMJh0+zqTGXGshyEGRsU2ImqWv6UuHGkHl6kEo= +github.com/aws/aws-sdk-go-v2/service/sts v1.16.17 h1:LVM2jzEQ8mhb2dhrFl4PJ3sa5+KcKT01dsMk2Ma9/FU= +github.com/aws/aws-sdk-go-v2/service/sts v1.16.17/go.mod h1:bQujK1n0V1D1Gz5uII1jaB1WDvhj4/T3tElsJnVXCR0= +github.com/aws/smithy-go v1.13.2 h1:TBLKyeJfXTrTXRHmsv4qWt9IQGYyWThLYaJWSahTOGE= +github.com/aws/smithy-go v1.13.2/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/aws/aws-sdk-go-v2 v1.17.3 h1:shN7NlnVzvDUgPQ+1rLMSxY8OWRNDRYtiqe0p/PgrhY= github.com/aws/aws-sdk-go-v2 v1.17.3/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= github.com/aws/aws-sdk-go-v2/config v1.18.8 h1:lDpy0WM8AHsywOnVrOHaSMfpaiV2igOw8D7svkFkXVA= @@ -56,6 +93,10 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= +github.com/dghubble/go-twitter v0.0.0-20220816163853-8a0df96f1e6d h1:qiUGPQxwkgoeDXtYaBEioXLEHffmBsRkM/9eum0vLS4= +github.com/dghubble/go-twitter v0.0.0-20220816163853-8a0df96f1e6d/go.mod h1:q7VYuSasPO79IE/QBNAMYVNlzZNy4Zr7vay6is50u5I= +github.com/dghubble/oauth1 v0.7.1 h1:JjbOVSVVkms9A4h/sTQy5Jb2nFuAAVb2qVYgenJPyrE= +github.com/dghubble/oauth1 v0.7.1/go.mod h1:0eEzON0UY/OLACQrmnjgJjmvCGXzjBCsZqL1kWDXtF0= github.com/dghubble/oauth1 v0.7.2 h1:pwcinOZy8z6XkNxvPmUDY52M7RDPxt0Xw1zgZ6Cl5JA= github.com/dghubble/oauth1 v0.7.2/go.mod h1:9erQdIhqhOHG/7K9s/tgh9Ks/AfoyrO5mW/43Lu2+kE= github.com/dghubble/sling v1.4.0 h1:/n8MRosVTthvMbwlNZgLx579OGVjUOy3GNEv5BIqAWY= @@ -77,6 +118,8 @@ github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-lark/lark v1.7.3 h1:02PupA3383fc+Ym/nOhZC8g7OaYrDzXzl0zjDpgOR1k= +github.com/go-lark/lark v1.7.3/go.mod h1:6ltbSztPZRT6IaO9ZIQyVaY5pVp/KeMizDYtfZkU+vM= github.com/go-lark/lark v1.7.4 h1:W+uMqLVnvadDuTrTx1LZUDftycYcLfeedvHtSuge40I= github.com/go-lark/lark v1.7.4/go.mod h1:6ltbSztPZRT6IaO9ZIQyVaY5pVp/KeMizDYtfZkU+vM= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= @@ -145,9 +188,14 @@ github.com/kevinburke/go-types v0.0.0-20210723172823-2deba1f80ba7 h1:K8qael4Lems github.com/kevinburke/go-types v0.0.0-20210723172823-2deba1f80ba7/go.mod h1:/Pk5i/SqYdYv1cie5wGwoZ4P6TpgMi+Yf58mtJSHdOw= github.com/kevinburke/rest v0.0.0-20210506044642-5611499aa33c h1:hnbwWED5rIu+UaMkLR3JtnscMVGqp35lfzQwLuZAAUY= github.com/kevinburke/rest v0.0.0-20210506044642-5611499aa33c/go.mod h1:pD+iEcdAGVXld5foVN4e24zb/6fnb60tgZPZ3P/3T/I= +github.com/kevinburke/twilio-go v0.0.0-20220615032439-b0fe9b151b0e h1:2HUamy+op/UxwJxDIg19oy/tIO/2M2tSasvihvhex4s= +github.com/kevinburke/twilio-go v0.0.0-20220615032439-b0fe9b151b0e/go.mod h1:PDdDH7RSKjjy9iFyoMzfeChOSmXpXuMEUqmAJSihxx4= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kevinburke/twilio-go v0.0.0-20221122012537-65f3dd7539e2 h1:k+lYMvS9cAl7e4Ea78qodfa6QZfXNa4QlFS/0GYpanI= github.com/kevinburke/twilio-go v0.0.0-20221122012537-65f3dd7539e2/go.mod h1:PDdDH7RSKjjy9iFyoMzfeChOSmXpXuMEUqmAJSihxx4= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/line/line-bot-sdk-go v7.8.0+incompatible h1:Uf9/OxV0zCVfqyvwZPH8CrdiHXXmMRa/L91G3btQblQ= github.com/line/line-bot-sdk-go v7.8.0+incompatible/go.mod h1:0RjLjJEAU/3GIcHkC3av6O4jInAbt25nnZVmOFUgDBg= @@ -178,15 +226,20 @@ github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1-0.20161029093637-248dadf4e906/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/plivo/plivo-go/v7 v7.11.0 h1:nOaRybcTdCJc5C0apDTSbiGTUgp3NIy8r9aGuR7uUoE= +github.com/plivo/plivo-go/v7 v7.11.0/go.mod h1:Jw1e16x0WjW334botVeKQT4hcMXMN55r4HH0XoGsR6Q= github.com/plivo/plivo-go/v7 v7.16.0 h1:xxVv4cSCaHapwd0UmFXE3q9M1fOGpeRzEa+0hj3M6h4= github.com/plivo/plivo-go/v7 v7.16.0/go.mod h1:Jw1e16x0WjW334botVeKQT4hcMXMN55r4HH0XoGsR6Q= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0= github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= github.com/sendgrid/sendgrid-go v3.12.0+incompatible h1:/N2vx18Fg1KmQOh6zESc5FJB8pYwt5QFBDflYPh1KVg= @@ -269,6 +322,8 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde h1:ejfdSekXMDxDLbRrJMwUk6KnSLZ2McaUCVcIKM+N6jc= +golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -317,8 +372,11 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= diff --git a/service/http/README.md b/service/http/README.md new file mode 100644 index 00000000..1f8d81d3 --- /dev/null +++ b/service/http/README.md @@ -0,0 +1,78 @@ +# Generic HTTP service + +[![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/nikoksr/notify/service/http) + +## Prerequisites + +Technically, you don't need any prerequisites to use this service. However, you will need an HTTP endpoint to send requests to. + +See our [Notify Test Server](https://github.com/nikoksr/notify-http-test) for a simple HTTP server that can be used for testing. + +## Usage + +```go +package main + +import ( + "context" + "log" + stdhttp "net/http" + + "github.com/nikoksr/notify" + "github.com/nikoksr/notify/service/http" +) + +func main() { + // Create http service + httpService := http.New() + + // + // In the following example, we will send two requests to the same HTTP endpoint. This is meant to be used with + // Notify's test http server: https://github.com/nikoksr/notify-http-test. It supports multiple content-types on the + // same endpoint, to simplify testing. All you have to do is run `go run main.go` in the test server's directory and + // this example will work. + // So the following example should send two requests to the same endpoint, one with content-type application/json and + // one with content-type text/plain. The requests should be logged differently by the test server since we provide + // a custom payload builder func for the second webhook. + + // Add a default webhook; this uses application/json as content type and POST as request method. + httpService.AddReceiversURLs("http://localhost:8080") + + // Add a custom webhook; the build payload function is used to build the payload that will be sent to the receiver + // from the given subject and message. + httpService.AddReceivers(&http.Webhook{ + URL: "http://localhost:8080", + ContentType: "text/plain", + Method: stdhttp.MethodPost, + BuildPayload: func(subject, message string) (payload any) { + return "[text/plain]: " + subject + " - " + message + }, + }) + + // + // NOTE: In case of an unsupported content type, we could provide a custom marshaller here. + // See http.Service.Serializer and http.defaultMarshaller.Marshal for details. + // + + // Add pre-send hook to log the request before it is sent. + httpService.PreSend(func(ctx context.Context, req *stdhttp.Request) error { + log.Printf("Sending request to %s", req.URL) + return nil + }) + + // Add post-send hook to log the response after it is received. + httpService.PostSend(func(ctx context.Context, req *stdhttp.Request, resp *stdhttp.Response) error { + log.Printf("Received response from %s", resp.Request.URL) + return nil + }) + + // Create the notifier and use the HTTP service + n := notify.NewWithServices(httpService) + + // Send a test message. + if err := n.Send(context.Background(), "Testing new features", "Notify's HTTP service is here."); err != nil { + log.Fatal(err) + } +} + +``` diff --git a/service/http/doc.go b/service/http/doc.go new file mode 100644 index 00000000..4aeae38a --- /dev/null +++ b/service/http/doc.go @@ -0,0 +1,76 @@ +/* +Package http provides an HTTP service. It is used to send notifications to HTTP endpoints. The service is configured +with a list of webhooks. Each webhook contains the URL of the endpoint, the HTTP method to use, the content type of the +HTTP request and a function that builds the payload of the request. + +The service also allows to register pre and post send hooks. These hooks are called before and after the request is sent +to the receiver. The pre send hook can be used to modify the request before it is sent. The post send hook can be used to +modify the response after it is received. The hooks are called in the order they are registered. + +Usage: + + package main + + import ( + "context" + "log" + stdhttp "net/http" + + "github.com/nikoksr/notify" + "github.com/nikoksr/notify/service/http" + ) + + func main() { + // Create http service + httpService := http.New() + + // + // In the following example, we will send two requests to the same HTTP endpoint. This is meant to be used with + // Notify's test http server: https://github.com/nikoksr/notify-http-test. It supports multiple content-types on the + // same endpoint, to simplify testing. All you have to do is run `go run main.go` in the test server's directory and + // this example will work. + // So the following example should send two requests to the same endpoint, one with content-type application/json and + // one with content-type text/plain. The requests should be logged differently by the test server since we provide + // a custom payload builder func for the second webhook. + + // Add a default webhook; this uses application/json as content type and POST as request method. + httpService.AddReceiversURLs("http://localhost:8080") + + // Add a custom webhook; the build payload function is used to build the payload that will be sent to the receiver + // from the given subject and message. + httpService.AddReceivers(&http.Webhook{ + URL: "http://localhost:8080", + ContentType: "text/plain", + Method: stdhttp.MethodPost, + BuildPayload: func(subject, message string) (payload any) { + return "[text/plain]: " + subject + " - " + message + }, + }) + + // + // NOTE: In case of an unsupported content type, we could provide a custom marshaller here. + // See http.Service.Serializer and http.defaultMarshaller.Marshal for details. + // + + // Add pre-send hook to log the request before it is sent. + httpService.PreSend(func(ctx context.Context, req *stdhttp.Request) error { + log.Printf("Sending request to %s", req.URL) + return nil + }) + + // Add post-send hook to log the response after it is received. + httpService.PostSend(func(ctx context.Context, req *stdhttp.Request, resp *stdhttp.Response) error { + log.Printf("Received response from %s", resp.Request.URL) + return nil + }) + + // Create the notifier and use the HTTP service + n := notify.NewWithServices(httpService) + + // Send a test message. + if err := n.Send(context.Background(), "Testing new features", "Notify's HTTP service is here."); err != nil { + log.Fatal(err) + } + } +*/ +package http diff --git a/service/http/http.go b/service/http/http.go new file mode 100644 index 00000000..77181cf3 --- /dev/null +++ b/service/http/http.go @@ -0,0 +1,279 @@ +package http + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "github.com/pkg/errors" + + "github.com/nikoksr/notify" +) + +type ( + // PreSendHookFn defines a function signature for a pre-send hook. + PreSendHookFn func(req *http.Request) error + + // PostSendHookFn defines a function signature for a post-send hook. + PostSendHookFn func(req *http.Request, resp *http.Response) error + + // BuildPayloadFn defines a function signature for a function that builds a payload. + BuildPayloadFn func(subject, message string) (payload any) + + // Serializer is used to serialize the payload to a byte slice. + Serializer interface { + Marshal(contentType string, payload any) (payloadRaw []byte, err error) + } + + // Webhook represents a single webhook receiver. It contains all the information needed to send a valid request to + // the receiver. The BuildPayload function is used to build the payload that will be sent to the receiver from the + // given subject and message. + Webhook struct { + ContentType string + Header http.Header + Method string + URL string + BuildPayload BuildPayloadFn + } + + // Service is the main struct of this package. It contains all the information needed to send notifications to a + // list of receivers. The receivers are represented by Webhooks and are expected to be valid HTTP endpoints. The + // Service also allows + Service struct { + client *http.Client + webhooks []*Webhook + preSendHooks []PreSendHookFn + postSendHooks []PostSendHookFn + Serializer Serializer + } +) + +const ( + defaultUserAgent = "notify/" + notify.Version + defaultContentType = "application/json; charset=utf-8" + defaultRequestMethod = http.MethodPost + + // Defining these as constants for testing purposes. + defaultSubjectKey = "subject" + defaultMessageKey = "message" +) + +type defaultMarshaller struct{} + +// Marshal takes a payload and serializes it to a byte slice. The content type is used to determine the serialization +// format. If the content type is not supported, an error is returned. The default marshaller supports the following +// content types: application/json, text/plain. +// NOTE: should we expand the default marshaller to support more content types? +func (defaultMarshaller) Marshal(contentType string, payload any) (out []byte, err error) { + switch { + case strings.HasPrefix(contentType, "application/json"): + out, err = json.Marshal(payload) + if err != nil { + return nil, errors.Wrap(err, "marshal json") + } + case strings.HasPrefix(contentType, "text/plain"): + str, ok := payload.(string) + if !ok { + return nil, errors.Errorf("payload was expected to be string, but was %T", payload) + } + out = []byte(str) + default: + return nil, errors.New("unsupported content type") + } + + return out, nil +} + +// buildDefaultPayload is the default payload builder. It builds a payload that is a map with the keys "subject" and +// "message". +func buildDefaultPayload(subject, message string) any { + return map[string]string{ + defaultSubjectKey: subject, + defaultMessageKey: message, + } +} + +// New returns a new instance of a Service notification service. Parameter 'tag' is used as a log prefix and may be left +// empty, it has a fallback value. +func New() *Service { + return &Service{ + client: http.DefaultClient, + webhooks: []*Webhook{}, + preSendHooks: []PreSendHookFn{}, + postSendHooks: []PostSendHookFn{}, + Serializer: defaultMarshaller{}, + } +} + +func newWebhook(url string) *Webhook { + return &Webhook{ + ContentType: defaultContentType, + Header: http.Header{}, + Method: defaultRequestMethod, + URL: url, + BuildPayload: buildDefaultPayload, + } +} + +// String returns a string representation of the webhook. It implements the fmt.Stringer interface. +func (w *Webhook) String() string { + if w == nil { + return "" + } + + return strings.TrimSpace(fmt.Sprintf("%s %s %s", strings.ToUpper(w.Method), w.URL, w.ContentType)) +} + +// AddReceivers accepts a list of Webhooks and adds them as receivers. The Webhooks are expected to be valid HTTP +// endpoints. +func (s *Service) AddReceivers(webhooks ...*Webhook) { + s.webhooks = append(s.webhooks, webhooks...) +} + +// AddReceiversURLs accepts a list of URLs and adds them as receivers. Internally it converts the URLs to Webhooks by +// using the default content-type ("application/json") and request method ("POST"). +func (s *Service) AddReceiversURLs(urls ...string) { + for _, url := range urls { + s.AddReceivers(newWebhook(url)) + } +} + +// WithClient sets the http client to be used for sending requests. Calling this method is optional, the default client +// will be used if this method is not called. +func (s *Service) WithClient(client *http.Client) { + if client != nil { + s.client = client + } +} + +// doPreSendHooks executes all the pre-send hooks. If any of the hooks returns an error, the execution is stopped and +// the error is returned. +func (s *Service) doPreSendHooks(req *http.Request) error { + for _, hook := range s.preSendHooks { + if err := hook(req); err != nil { + return err + } + } + + return nil +} + +// doPostSendHooks executes all the post-send hooks. If any of the hooks returns an error, the execution is stopped and +// the error is returned. +func (s *Service) doPostSendHooks(req *http.Request, resp *http.Response) error { + for _, hook := range s.postSendHooks { + if err := hook(req, resp); err != nil { + return err + } + } + + return nil +} + +// PreSend adds a pre-send hook to the service. The hook will be executed before sending a request to a receiver. +func (s *Service) PreSend(hook PreSendHookFn) { + s.preSendHooks = append(s.preSendHooks, hook) +} + +// PostSend adds a post-send hook to the service. The hook will be executed after sending a request to a receiver. +func (s *Service) PostSend(hook PostSendHookFn) { + s.postSendHooks = append(s.postSendHooks, hook) +} + +// newRequest creates a new http request with the given method, content-type, url and payload. Request created by this +// function will usually be passed to the Service.do method. +func newRequest(ctx context.Context, hook *Webhook, payload io.Reader) (*http.Request, error) { + req, err := http.NewRequestWithContext(ctx, hook.Method, hook.URL, payload) + if err != nil { + return nil, err + } + + req.Header = hook.Header + + if req.Header.Get("User-Agent") == "" { + req.Header.Set("User-Agent", defaultUserAgent) + } + if req.Header.Get("Content-Type") == "" { + req.Header.Set("Content-Type", hook.ContentType) + } + + return req, nil +} + +// do sends the given request and returns an error if the request failed. A failed request gets identified by either +// an unsuccessful status code or a non-nil error. The given request is expected to be valid and was usually created +// by the newRequest function. +func (s *Service) do(req *http.Request) error { + // Execute all pre-send hooks in order. + if err := s.doPreSendHooks(req); err != nil { + return errors.Wrap(err, "pre-send hooks") + } + + // Actually send the HTTP request. + resp, err := s.client.Do(req) + if err != nil { + return err + } + defer func() { _ = resp.Body.Close() }() + + // Execute all post-send hooks in order. + if err = s.doPostSendHooks(req, resp); err != nil { + return errors.Wrap(err, "post-send hooks") + } + + // Check if response code is 2xx. Should this be configurable? + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("responded with status code: %d", resp.StatusCode) + } + + return nil +} + +// send is a helper method that sends a message to a single webhook. It wraps the core logic of the Send method, which +// is creating a new request for the given webhook and sending it. +func (s *Service) send(ctx context.Context, webhook *Webhook, payload []byte) error { + // Create a new HTTP request for the given webhook. + req, err := newRequest(ctx, webhook, bytes.NewReader(payload)) + if err != nil { + return errors.Wrapf(err, "create request %q", webhook) + } + defer func() { _ = req.Body.Close() }() + + return s.do(req) +} + +// Send takes a message and sends it to all webhooks. +func (s *Service) Send(ctx context.Context, subject, message string) error { + // Send message to all webhooks. + for _, webhook := range s.webhooks { + select { + case <-ctx.Done(): + return ctx.Err() + default: + // Skip webhook if it is nil. + if webhook == nil { + continue + } + + // Build the payload for the current webhook. + payload := webhook.BuildPayload(subject, message) + + // Marshal the message into a payload. + payloadRaw, err := s.Serializer.Marshal(webhook.ContentType, payload) + if err != nil { + return errors.Wrap(err, "marshal payload") + } + + // Send the payload to the webhook. + if err = s.send(ctx, webhook, payloadRaw); err != nil { + return errors.Wrapf(err, "send request %q", webhook) + } + } + } + + return nil +} diff --git a/service/http/http_test.go b/service/http/http_test.go new file mode 100644 index 00000000..60a5af19 --- /dev/null +++ b/service/http/http_test.go @@ -0,0 +1,446 @@ +package http + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/pkg/errors" + + "github.com/stretchr/testify/assert" +) + +// Set up a test server to handle the requests +var notifyServer *httptest.Server + +// Allows us to simulate an error returned from the server on a per-request basis +const headerTestError = "X-Test-Error" + +func TestMain(m *testing.M) { + var notifyHandler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Header.Get(headerTestError) == "true": + w.WriteHeader(http.StatusInternalServerError) + default: + w.WriteHeader(http.StatusOK) + } + }) + + notifyServer = httptest.NewServer(notifyHandler) + defer notifyServer.Close() + + os.Exit(m.Run()) +} + +// Create a custom serializer that will return an error +type errorSerializer struct{} + +// Marshal is a no-op and always returns an error. +func (errorSerializer) Marshal(_ string, _ any) (payloadRaw []byte, err error) { + return nil, errors.New("error") +} + +func TestNew(t *testing.T) { + t.Parallel() + + s1 := New() + assert.NotNil(t, s1, "service should not be nil") + + s2 := New() + assert.NotNil(t, s2, "service should not be nil") + assert.Equal(t, s1, s2, "services should be equal") +} + +func TestService_WithClient(t *testing.T) { + t.Parallel() + + service := New() + assert.NotNil(t, service, "service should not be nil") + assert.NotNil(t, service.client, "client should not be nil") + + // Create a new client + client := &http.Client{} + service.WithClient(client) + assert.Equal(t, client, service.client, "clients should be equal") + + // Nil client should not change the service client + service.WithClient(nil) + assert.Equal(t, client, service.client, "clients should be equal") +} + +func TestService_AddReceivers(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + service *Service + urls []string + }{ + { + name: "test case 1", + service: New(), + urls: []string{ + "http://localhost:8080", + "http://localhost:8081", + "http://localhost:8082", + }, + }, + { + name: "test case 2", + service: New(), + urls: []string{}, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + tt.service.AddReceiversURLs(tt.urls...) + assert.Equal(t, len(tt.urls), len(tt.service.webhooks), "webhooks should be equal") + + for i, hook := range tt.urls { + assert.Equal(t, hook, tt.service.webhooks[i].URL, "webhooks should be equal") + } + }) + } +} + +func TestService_Hooks(t *testing.T) { + t.Parallel() + + // Set the local server as the receiver + service := New() + service.AddReceiversURLs(notifyServer.URL) + + // Constants for the test + const ( + testSubject = "test subject" + testMessage = "test message" + ) + + // Add a very simple pre-send hook. We'll check if the header and body are set correctly. + service.PreSend(func(req *http.Request) error { + // At this point, the request should be unmodified as this is the first hook. Unmarshal the bodyRaw and check the + // subject and message. + bodyRaw, err := io.ReadAll(req.Body) + if err != nil { + return errors.Wrap(err, "failed to read request body") + } + + var body map[string]string + if err := json.Unmarshal(bodyRaw, &body); err != nil { + return errors.Wrap(err, "failed to unmarshal request body") + } + + // This implicitly checks the correctness of buildDefaultPayload. + assert.Equal(t, testSubject, body[defaultSubjectKey], "subject should be equal") + assert.Equal(t, testMessage, body[defaultMessageKey], "message should be equal") + + // Injecting new headers and bodyRaw + req.Header.Set("X-Test-1", "test-header") + req.Header.Set("Content-Type", "text/plain") + req.Body = io.NopCloser(bytes.NewBuffer([]byte("test-body"))) + + return nil + }) + + // Adding a second pre-send hook. We'll check if the header and body have been correctly modified by the first hook. + service.PreSend(func(req *http.Request) error { + // Check the headers + assert.Equal(t, "test-header", req.Header.Get("X-Test-1"), "header should be equal") + assert.Equal(t, "text/plain", req.Header.Get("Content-Type"), "header should be equal") + + // Check the bodyRaw + bodyRaw, err := io.ReadAll(req.Body) + if err != nil { + return errors.Wrap(err, "failed to read request bodyRaw") + } + assert.Equal(t, "test-body", string(bodyRaw), "body should be equal") + + // Make sure the body is reset to the original value + req.Body = io.NopCloser(bytes.NewBuffer(bodyRaw)) + + // Also, refresh the Content-Length header. This is required because we've modified the bodyRaw and the test + // would fail otherwise. + req.ContentLength = int64(len(bodyRaw)) + + // Injecting a new header to confirm that consecutive hooks work as expected + req.Header.Set("X-Test-2", "test-header-2") + req.Header.Del("X-Test-1") + + return nil + }) + + // Adding a third pre-send hook. We'll check if the header and body have been correctly modified by the first two hooks. + service.PreSend(func(req *http.Request) error { + assert.Equal(t, "test-header-2", req.Header.Get("X-Test-2"), "header should be equal") + assert.Equal(t, "", req.Header.Get("X-Test-1"), "header should be equal") + + // Modifying the headers one last time to verify that the post-send hook works as expected + req.Header.Set("X-Test-3", "test-header-3") + req.Header.Del("X-Test-2") + + return nil + }) + + // Add a very simple post-send hook. We'll inject a custom header and return an error, in case the according http + // header has been set. + service.PostSend(func(req *http.Request, res *http.Response) error { + res.Header.Set("X-Test-1", "test-header") + + return nil + }) + + // Add a second post-send hook. We'll check if the header has been correctly modified by the first hook. + service.PostSend(func(req *http.Request, res *http.Response) error { + assert.Equal(t, "test-header", res.Header.Get("X-Test-1"), "header should be equal") + + // Injecting a new header to confirm that consecutive hooks work as expected + res.Header.Set("X-Test-2", "test-header-2") + res.Header.Del("X-Test-1") + + return nil + }) + + // Add a third post-send hook. We'll check if the header has been correctly modified by the first two hooks. + service.PostSend(func(req *http.Request, res *http.Response) error { + assert.Equal(t, "test-header-2", res.Header.Get("X-Test-2"), "header should be equal") + assert.Equal(t, "", res.Header.Get("X-Test-1"), "header should be equal") + + return nil + }) + + // Sanity check + assert.Equal(t, 3, len(service.preSendHooks), "preSendHooks should be equal") + assert.Equal(t, 3, len(service.postSendHooks), "postSendHooks should be equal") + + // Send a notification + err := service.Send(context.Background(), testSubject, testMessage) + assert.NoError(t, err, "error should be nil") + + // Now, add a new pre-send hook that sets special header that requests the server to return an error. We'll check if + // the error is correctly returned. + service.PreSend(func(req *http.Request) error { + req.Header.Set(headerTestError, "true") + + return nil + }) + + // Send a notification + err = service.Send(context.Background(), testSubject, testMessage) + assert.Error(t, err, "error should not be nil") + + // Reset the hooks + service.preSendHooks = make([]PreSendHookFn, 0) + service.postSendHooks = make([]PostSendHookFn, 0) + + // Add a pre-send hook that returns an error + service.PreSend(func(req *http.Request) error { + return errors.New("test error") + }) + + // Send a notification + err = service.Send(context.Background(), testSubject, testMessage) + assert.Error(t, err, "error should not be nil") + + // Reset the hooks again and add a post-send hook that returns an error + service.preSendHooks = make([]PreSendHookFn, 0) + + service.PostSend(func(req *http.Request, res *http.Response) error { + return errors.New("test error") + }) + + // Send a notification + err = service.Send(context.Background(), testSubject, testMessage) + assert.Error(t, err, "error should not be nil") +} + +func TestService_Send(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Create service with local server as receiver + service := New() + service.AddReceiversURLs(notifyServer.URL) + + // Sending this notification should work without any issues + err := service.Send(ctx, "test subject", "test message") + assert.NoError(t, err, "error should be nil") + + // Now, let's reset the receivers and set a custom one, specifically requesting for our test server to return an + // error. This should result in an error. + service.webhooks = make([]*Webhook, 0) + + header := http.Header{} + header.Set(headerTestError, "true") + + service.AddReceivers(&Webhook{ + ContentType: defaultContentType, + Header: header, + Method: http.MethodPost, + URL: notifyServer.URL, + BuildPayload: buildDefaultPayload, + }) + + err = service.Send(ctx, "test subject", "test message") + assert.Error(t, err, "error should not be nil") + + // Reset again, add a functioning receiver again for further tests + service.webhooks = make([]*Webhook, 0) + service.AddReceiversURLs(notifyServer.URL) + + // Since we won't reset the receivers list again, add a nil receiver to make sure that the service doesn't crash. + service.AddReceivers(nil) + + err = service.Send(ctx, "test subject", "test message") + assert.NoError(t, err, "error should be nil") + + // Test setting a custom marshaller that always returns an error + service.Serializer = errorSerializer{} + + err = service.Send(ctx, "test subject", "test message") + assert.Error(t, err, "error should not be nil") + + // Test context cancellation. + cancel() // Cancel the context + + err = service.Send(ctx, "test subject", "test message") + assert.Error(t, err, "error should not be nil") +} + +func Test_newWebhook(t *testing.T) { + t.Parallel() + + hook1 := newWebhook("https://example.com") + assert.NotNil(t, hook1, "hook1 should not be nil") + + hook2 := newWebhook("https://example.com") + assert.NotNil(t, hook2, "hook2 should not be nil") + + assert.NotEqual(t, hook1, hook2, "hooks should not be equal") +} + +func TestWebhook_String(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + hook *Webhook + want string + }{ + { + name: "empty", + hook: &Webhook{}, + want: "", + }, + { + name: "nil", + hook: nil, + want: "", + }, + { + name: "test case 1", + hook: newWebhook("https://example.com"), + want: "POST https://example.com application/json; charset=utf-8", + }, + { + name: "test case 2", + hook: &Webhook{ + Method: http.MethodGet, // Doesn't have to make sense, but it's just for testing + URL: "https://example.com", + ContentType: "text/plain", + }, + want: "GET https://example.com text/plain", + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + assert.Equalf(t, tt.want, tt.hook.String(), "String() = %v, want %v", tt.hook.String(), tt.want) + }) + } +} + +func Test_defaultMarshaller_Marshal(t *testing.T) { + t.Parallel() + + type args struct { + contentType string + payload any + } + tests := []struct { + name string + args args + wantOut []byte + wantErr assert.ErrorAssertionFunc + }{ + { + name: "test marshal valid json", + args: args{ + contentType: "application/json", + payload: map[string]interface{}{"test": "test"}, + }, + wantOut: []byte(`{"test":"test"}`), + wantErr: assert.NoError, + }, + { + name: "test marshal invalid json", + args: args{ + contentType: "application/json", + payload: map[string]interface{}{"test": make(chan int)}, + }, + wantOut: nil, + wantErr: assert.Error, + }, + { + name: "test marshal valid text", + args: args{ + contentType: "text/plain", + payload: "test", + }, + wantOut: []byte("test"), + wantErr: assert.NoError, + }, + { + name: "test marshal invalid text", + args: args{ + contentType: "text/plain", + payload: map[string]interface{}{"test": "test"}, + }, + wantOut: nil, + wantErr: assert.Error, + }, + { + name: "test marshal invalid content type", + args: args{ + contentType: "invalid", + payload: map[string]interface{}{"test": "test"}, + }, + wantOut: nil, + wantErr: assert.Error, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + serializer := defaultMarshaller{} + gotOut, err := serializer.Marshal(tt.args.contentType, tt.args.payload) + if !tt.wantErr(t, err, fmt.Sprintf("Marshal(%v, %v)", tt.args.contentType, tt.args.payload)) { + return + } + assert.Equalf(t, tt.wantOut, gotOut, "Marshal(%v, %v)", tt.args.contentType, tt.args.payload) + }) + } +} diff --git a/version.go b/version.go new file mode 100644 index 00000000..0f3796f5 --- /dev/null +++ b/version.go @@ -0,0 +1,4 @@ +package notify + +// Version is the current version of the library. +const Version = "unknown"