Add web push tests
@ -194,7 +194,7 @@ func execServe(c *cli.Context) error {
|
||||
if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) {
|
||||
return errors.New("if set, FCM key file must exist")
|
||||
} else if webPushEnabled && (webPushPrivateKey == "" || webPushPublicKey == "" || webPushSubscriptionsFile == "" || webPushEmailAddress == "" || baseURL == "") {
|
||||
return errors.New("if web push is enabled, web-push-private-key, web-push-public-key, web-push-subscriptions-file, web-push-email-address, and base-url should be set. run 'ntfy web-push-keys' to generate keys")
|
||||
return errors.New("if web push is enabled, web-push-private-key, web-push-public-key, web-push-subscriptions-file, web-push-email-address, and base-url should be set. run 'ntfy web-push generate-keys' to generate keys")
|
||||
} else if keepaliveInterval < 5*time.Second {
|
||||
return errors.New("keepalive interval cannot be lower than five seconds")
|
||||
} else if managerInterval < 5*time.Second {
|
||||
|
@ -14,11 +14,20 @@ func init() {
|
||||
}
|
||||
|
||||
var cmdWebPush = &cli.Command{
|
||||
Name: "web-push-keys",
|
||||
Usage: "Generate web push VAPID keys",
|
||||
UsageText: "ntfy web-push-keys",
|
||||
Name: "web-push",
|
||||
Usage: "Generate keys, in the future manage web push subscriptions",
|
||||
UsageText: "ntfy web-push [generate-keys]",
|
||||
Category: categoryServer,
|
||||
Action: generateWebPushKeys,
|
||||
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Action: generateWebPushKeys,
|
||||
Name: "generate-keys",
|
||||
Usage: "Generate VAPID keys to enable browser background push notifications",
|
||||
UsageText: "ntfy web-push generate-keys",
|
||||
Category: categoryServer,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func generateWebPushKeys(c *cli.Context) error {
|
||||
@ -27,13 +36,28 @@ func generateWebPushKeys(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(c.App.ErrWriter, `Add the following lines to your config file:
|
||||
fmt.Fprintf(c.App.ErrWriter, `Keys generated.
|
||||
|
||||
VAPID Public Key:
|
||||
%s
|
||||
|
||||
VAPID Private Key:
|
||||
%s
|
||||
|
||||
---
|
||||
|
||||
Add the following lines to your config file:
|
||||
|
||||
web-push-enabled: true
|
||||
web-push-public-key: %s
|
||||
web-push-private-key: %s
|
||||
web-push-subscriptions-file: <filename>
|
||||
web-push-email-address: <email address>
|
||||
`, publicKey, privateKey)
|
||||
|
||||
Look at the docs for other methods (e.g. command line flags & environment variables).
|
||||
|
||||
You will also need to set a base-url.
|
||||
`, publicKey, privateKey, publicKey, privateKey)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
24
cmd/web_push_test.go
Normal file
@ -0,0 +1,24 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/server"
|
||||
)
|
||||
|
||||
func TestCLI_WebPush_GenerateKeys(t *testing.T) {
|
||||
app, _, _, stderr := newTestApp()
|
||||
require.Nil(t, runWebPushCommand(app, server.NewConfig(), "generate-keys"))
|
||||
require.Contains(t, stderr.String(), "Keys generated.")
|
||||
}
|
||||
|
||||
func runWebPushCommand(app *cli.App, conf *server.Config, args ...string) error {
|
||||
webPushArgs := []string{
|
||||
"ntfy",
|
||||
"--log-level=ERROR",
|
||||
"web-push",
|
||||
}
|
||||
return app.Run(append(webPushArgs, args...))
|
||||
}
|
@ -1286,8 +1286,8 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
|
||||
| `stripe-webhook-key` | `NTFY_STRIPE_WEBHOOK_KEY` | *string* | - | Payments: Key required to validate the authenticity of incoming webhooks from Stripe |
|
||||
| `billing-contact` | `NTFY_BILLING_CONTACT` | *email address* or *website* | - | Payments: Email or website displayed in Upgrade dialog as a billing contact |
|
||||
| `web-push-enabled` | `NTFY_WEB_PUSH_ENABLED` | *boolean* (`true` or `false`) | - | Web Push: Enable/disable (requires private and public key below). |
|
||||
| `web-push-public-key` | `NTFY_WEB_PUSH_PUBLIC_KEY` | *string* | - | Web Push: Public Key. Run `ntfy web-push-keys` to generate |
|
||||
| `web-push-private-key` | `NTFY_WEB_PUSH_PRIVATE_KEY` | *string* | - | Web Push: Private Key. Run `ntfy web-push-keys` to generate |
|
||||
| `web-push-public-key` | `NTFY_WEB_PUSH_PUBLIC_KEY` | *string* | - | Web Push: Public Key. Run `ntfy web-push generate-keys` to generate |
|
||||
| `web-push-private-key` | `NTFY_WEB_PUSH_PRIVATE_KEY` | *string* | - | Web Push: Private Key. Run `ntfy web-push generate-keys` to generate |
|
||||
| `web-push-subscriptions-file` | `NTFY_WEB_PUSH_SUBSCRIPTIONS_FILE` | *string* | - | Web Push: Subscriptions file |
|
||||
| `web-push-email-address` | `NTFY_WEB_PUSH_EMAIL_ADDRESS` | *string* | - | Web Push: Sender email address |
|
||||
|
||||
|
@ -247,7 +247,7 @@ Reference: <https://stackoverflow.com/questions/34160509/options-for-testing-ser
|
||||
|
||||
#### With the dev servers
|
||||
|
||||
1. Get web push keys `go run main.go web-push-keys`
|
||||
1. Get web push keys `go run main.go web-push generate-keys`
|
||||
|
||||
2. Run the server with web push enabled
|
||||
|
||||
|
BIN
docs/static/img/pwa-badge.png
vendored
Normal file
After Width: | Height: | Size: 786 KiB |
BIN
docs/static/img/pwa-install.png
vendored
Normal file
After Width: | Height: | Size: 285 KiB |
BIN
docs/static/img/pwa.png
vendored
Normal file
After Width: | Height: | Size: 279 KiB |
BIN
docs/static/img/web-pin.png
vendored
Before Width: | Height: | Size: 18 KiB |
BIN
docs/static/img/web-push-settings.png
vendored
Normal file
After Width: | Height: | Size: 322 KiB |
BIN
docs/static/img/web-subscribe.png
vendored
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 294 KiB |
12
docs/subscribe/desktop.md
Normal file
@ -0,0 +1,12 @@
|
||||
# Using the web app as an installed PWA
|
||||
|
||||
While ntfy doesn't have a built desktop app, it is built as a progressive web app and can be installed.
|
||||
|
||||
This is supported on Chrome and Edge on desktop, as well as Chrome on Android and Safari on iOS.
|
||||
[caniuse reference](https://caniuse.com/web-app-manifest)
|
||||
|
||||
<div id="pwa-screenshots" class="screenshots">
|
||||
<a href="../../static/img/pwa.png"><img src="../../static/img/pwa.png"/></a>
|
||||
<a href="../../static/img/pwa-install.png"><img src="../../static/img/pwa-install.png"/></a>
|
||||
<a href="../../static/img/pwa-badge.png"><img src="../../static/img/pwa-badge.png"/></a>
|
||||
</div>
|
@ -1,7 +1,41 @@
|
||||
# Subscribe from the Web UI
|
||||
You can use the Web UI to subscribe to topics as well. If you do, and you keep the website open, **notifications will
|
||||
pop up as desktop notifications**. Simply type in the topic name and click the *Subscribe* button. The browser will
|
||||
keep a connection open and listen for incoming notifications.
|
||||
|
||||
You can use the Web UI to subscribe to topics as well. Simply type in the topic name and click the *Subscribe* button.
|
||||
|
||||
While subscribing, you have the option to enable desktop notifications, as well as background notifications. When you
|
||||
enable them for the first time, you will be prompted to allow notifications on your browser.
|
||||
|
||||
- **Sound only**
|
||||
|
||||
If you don't enable browser notifications, a sound will play when a new notification comes in, and the tab title
|
||||
will show the number of new notifications.
|
||||
|
||||
- **Browser Notifications**
|
||||
|
||||
This requires an active ntfy tab to be open to receive notifications. These are typically instantaneous, and will
|
||||
appear as a system notification. If you don't see these, check that your browser is allowed to show notifications
|
||||
(for example in System Settings on macOS).
|
||||
|
||||
If you don't want to enable background notifications, pinning the ntfy tab on your browser is a good solution to leave
|
||||
it running.
|
||||
|
||||
- **Background Notifications**
|
||||
|
||||
This uses the [Web Push API](https://caniuse.com/push-api). You don't need an active ntfy tab open, but in some
|
||||
cases you may need to keep your browser open.
|
||||
|
||||
|
||||
| Browser | Platform | Browser Running | Browser Not Running | Notes |
|
||||
| ------- | -------- | --------------- | ------------------- | ------------------------------------------------------- |
|
||||
| Chrome | Desktop | ✅ | ❌ | |
|
||||
| Firefox | Desktop | ✅ | ❌ | |
|
||||
| Edge | Desktop | ✅ | ❌ | |
|
||||
| Opera | Desktop | ✅ | ❌ | |
|
||||
| Safari | Desktop | ✅ | ✅ | requires Safari 16.1, macOS 13 Ventura |
|
||||
| Chrome | Android | ✅ | ✅ | |
|
||||
| Safari | iOS | ⚠️ | ⚠️ | requires iOS 16.4, only when app is added to homescreen |
|
||||
|
||||
(Browsers below 1% usage not shown, look at the Push API link for more info)
|
||||
|
||||
To learn how to send messages, check out the [publishing page](../publish.md).
|
||||
|
||||
@ -11,17 +45,16 @@ To learn how to send messages, check out the [publishing page](../publish.md).
|
||||
<a href="../../static/img/web-subscribe.png"><img src="../../static/img/web-subscribe.png"/></a>
|
||||
</div>
|
||||
|
||||
To keep receiving desktop notifications from ntfy, you need to keep the website open. What I do, and what I highly recommend,
|
||||
is to pin the tab so that it's always open, but sort of out of the way:
|
||||
|
||||
<figure markdown>
|
||||
![pinned](../static/img/web-pin.png){ width=500 }
|
||||
<figcaption>Pin web app to move it out of the way</figcaption>
|
||||
</figure>
|
||||
|
||||
If topic reservations are enabled, you can claim ownership over topics and define access to it:
|
||||
|
||||
<div id="reserve-screenshots" class="screenshots">
|
||||
<a href="../../static/img/web-reserve-topic.png"><img src="../../static/img/web-reserve-topic.png"/></a>
|
||||
<a href="../../static/img/web-reserve-topic-dialog.png"><img src="../../static/img/web-reserve-topic-dialog.png"/></a>
|
||||
</div>
|
||||
|
||||
You can set your default choice for new subscriptions (for example synced account subscriptions and the default toggle state)
|
||||
in the settings page:
|
||||
|
||||
<div id="push-settings-screenshots" class="screenshots">
|
||||
<a href="../../static/img/web-push-settings.png"><img src="../../static/img/web-push-settings.png"/></a>
|
||||
</div>
|
||||
|
@ -82,6 +82,7 @@ nav:
|
||||
- "Subscribing":
|
||||
- "From your phone": subscribe/phone.md
|
||||
- "From the Web app": subscribe/web.md
|
||||
- "From the Desktop": subscribe/desktop.md
|
||||
- "From the CLI": subscribe/cli.md
|
||||
- "Using the API": subscribe/api.md
|
||||
- "Self-hosting":
|
||||
|
@ -233,8 +233,10 @@ func NewConfig() *Config {
|
||||
EnableReservations: false,
|
||||
AccessControlAllowOrigin: "*",
|
||||
Version: "",
|
||||
WebPushEnabled: false,
|
||||
WebPushPrivateKey: "",
|
||||
WebPushPublicKey: "",
|
||||
WebPushSubscriptionsFile: "",
|
||||
WebPushEmailAddress: "",
|
||||
}
|
||||
}
|
||||
|
@ -77,7 +77,7 @@ var (
|
||||
rawPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/raw$`)
|
||||
wsPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/ws$`)
|
||||
authPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/auth$`)
|
||||
webPushPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/web-push$`)
|
||||
webPushSubscribePathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/web-push/subscribe$`)
|
||||
webPushUnsubscribePathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/web-push/unsubscribe$`)
|
||||
publishPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/(publish|send|trigger)$`)
|
||||
|
||||
@ -535,7 +535,7 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
|
||||
return s.limitRequests(s.authorizeTopicRead(s.handleSubscribeWS))(w, r, v)
|
||||
} else if r.Method == http.MethodGet && authPathRegex.MatchString(r.URL.Path) {
|
||||
return s.limitRequests(s.authorizeTopicRead(s.handleTopicAuth))(w, r, v)
|
||||
} else if r.Method == http.MethodPost && webPushPathRegex.MatchString(r.URL.Path) {
|
||||
} else if r.Method == http.MethodPost && webPushSubscribePathRegex.MatchString(r.URL.Path) {
|
||||
return s.limitRequestsWithTopic(s.authorizeTopicRead(s.ensureWebPushEnabled(s.handleTopicWebPushSubscribe)))(w, r, v)
|
||||
} else if r.Method == http.MethodPost && webPushUnsubscribePathRegex.MatchString(r.URL.Path) {
|
||||
return s.limitRequestsWithTopic(s.authorizeTopicRead(s.ensureWebPushEnabled(s.handleTopicWebPushUnsubscribe)))(w, r, v)
|
||||
@ -985,7 +985,6 @@ func (s *Server) publishToWebPushEndpoints(v *visitor, m *message) {
|
||||
return
|
||||
}
|
||||
|
||||
failedCount := 0
|
||||
totalCount := len(subscriptions)
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
@ -1029,12 +1028,11 @@ func (s *Server) publishToWebPushEndpoints(v *visitor, m *message) {
|
||||
jsonPayload, err := json.Marshal(payload)
|
||||
|
||||
if err != nil {
|
||||
failedCount++
|
||||
logvm(v, m).Err(err).Fields(ctx).Debug("Unable to publish web push message")
|
||||
return
|
||||
}
|
||||
|
||||
_, err = webpush.SendNotification(jsonPayload, &sub.BrowserSubscription, &webpush.Options{
|
||||
resp, err := webpush.SendNotification(jsonPayload, &sub.BrowserSubscription, &webpush.Options{
|
||||
Subscriber: s.config.WebPushEmailAddress,
|
||||
VAPIDPublicKey: s.config.WebPushPublicKey,
|
||||
VAPIDPrivateKey: s.config.WebPushPrivateKey,
|
||||
@ -1044,26 +1042,29 @@ func (s *Server) publishToWebPushEndpoints(v *visitor, m *message) {
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
failedCount++
|
||||
logvm(v, m).Err(err).Fields(ctx).Debug("Unable to publish web push message")
|
||||
|
||||
// probably need to handle different codes differently,
|
||||
// but for now just expire the subscription on any error
|
||||
err = s.webPushSubscriptionStore.ExpireWebPushEndpoint(sub.BrowserSubscription.Endpoint)
|
||||
if err != nil {
|
||||
logvm(v, m).Err(err).Fields(ctx).Warn("Unable to expire subscription")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// May want to handle at least 429 differently, but for now treat all errors the same
|
||||
if !(200 <= resp.StatusCode && resp.StatusCode <= 299) {
|
||||
logvm(v, m).Fields(ctx).Field("response", resp).Debug("Unable to publish web push message")
|
||||
|
||||
err = s.webPushSubscriptionStore.ExpireWebPushEndpoint(sub.BrowserSubscription.Endpoint)
|
||||
if err != nil {
|
||||
logvm(v, m).Err(err).Fields(ctx).Warn("Unable to expire subscription")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}(i, xi)
|
||||
}
|
||||
|
||||
ctx = log.Context{"topic": m.Topic, "message_id": m.ID, "failed_count": failedCount, "total_count": totalCount}
|
||||
|
||||
if failedCount > 0 {
|
||||
logvm(v, m).Fields(ctx).Warn("Unable to publish web push messages to %d of %d endpoints", failedCount, totalCount)
|
||||
} else {
|
||||
logvm(v, m).Fields(ctx).Debug("Published %d web push messages successfully", totalCount)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, unifiedpush bool, err *errHTTP) {
|
||||
|
@ -40,7 +40,7 @@
|
||||
|
||||
# Enable web push
|
||||
#
|
||||
# Run ntfy web-push-keys to generate the keys
|
||||
# Run ntfy web-push generate-keys to generate the keys
|
||||
#
|
||||
# web-push-enabled: true
|
||||
# web-push-public-key: ""
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SherClockHolmes/webpush-go"
|
||||
"github.com/stretchr/testify/require"
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/util"
|
||||
@ -2604,14 +2605,35 @@ func newTestConfig(t *testing.T) *Config {
|
||||
return conf
|
||||
}
|
||||
|
||||
func newTestConfigWithAuthFile(t *testing.T) *Config {
|
||||
conf := newTestConfig(t)
|
||||
func configureAuth(t *testing.T, conf *Config) *Config {
|
||||
conf.AuthFile = filepath.Join(t.TempDir(), "user.db")
|
||||
conf.AuthStartupQueries = "pragma journal_mode = WAL; pragma synchronous = normal; pragma temp_store = memory;"
|
||||
conf.AuthBcryptCost = bcrypt.MinCost // This speeds up tests a lot
|
||||
return conf
|
||||
}
|
||||
|
||||
func newTestConfigWithAuthFile(t *testing.T) *Config {
|
||||
conf := newTestConfig(t)
|
||||
conf = configureAuth(t, conf)
|
||||
return conf
|
||||
}
|
||||
|
||||
func newTestConfigWithWebPush(t *testing.T) *Config {
|
||||
conf := newTestConfig(t)
|
||||
|
||||
privateKey, publicKey, err := webpush.GenerateVAPIDKeys()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
conf.WebPushEnabled = true
|
||||
conf.WebPushSubscriptionsFile = filepath.Join(t.TempDir(), "subscriptions.db")
|
||||
conf.WebPushEmailAddress = "testing@example.com"
|
||||
conf.WebPushPrivateKey = privateKey
|
||||
conf.WebPushPublicKey = publicKey
|
||||
return conf
|
||||
}
|
||||
|
||||
func newTestServer(t *testing.T, config *Config) *Server {
|
||||
server, err := New(config)
|
||||
if err != nil {
|
||||
|
212
server/server_web_push_test.go
Normal file
@ -0,0 +1,212 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/SherClockHolmes/webpush-go"
|
||||
"github.com/stretchr/testify/require"
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/util"
|
||||
)
|
||||
|
||||
var (
|
||||
webPushSubscribePayloadExample = `{
|
||||
"browser_subscription":{
|
||||
"endpoint": "https://example.com/webpush",
|
||||
"keys": {
|
||||
"p256dh": "p256dh-key",
|
||||
"auth": "auth-key"
|
||||
}
|
||||
}
|
||||
}`
|
||||
)
|
||||
|
||||
func TestServer_WebPush_GetConfig(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfigWithWebPush(t))
|
||||
|
||||
response := request(t, s, "GET", "/v1/web-push-config", "", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
require.Equal(t, fmt.Sprintf(`{"public_key":"%s"}`, s.config.WebPushPublicKey)+"\n", response.Body.String())
|
||||
}
|
||||
|
||||
func TestServer_WebPush_TopicSubscribe(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfigWithWebPush(t))
|
||||
|
||||
response := request(t, s, "POST", "/test-topic/web-push/subscribe", webPushSubscribePayloadExample, nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
require.Equal(t, `{"success":true}`+"\n", response.Body.String())
|
||||
|
||||
subs, err := s.webPushSubscriptionStore.GetSubscriptionsForTopic("test-topic")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
require.Len(t, subs, 1)
|
||||
require.Equal(t, subs[0].BrowserSubscription.Endpoint, "https://example.com/webpush")
|
||||
require.Equal(t, subs[0].BrowserSubscription.Keys.P256dh, "p256dh-key")
|
||||
require.Equal(t, subs[0].BrowserSubscription.Keys.Auth, "auth-key")
|
||||
require.Equal(t, subs[0].Username, "")
|
||||
}
|
||||
|
||||
func TestServer_WebPush_TopicSubscribeProtected_Allowed(t *testing.T) {
|
||||
config := configureAuth(t, newTestConfigWithWebPush(t))
|
||||
config.AuthDefault = user.PermissionDenyAll
|
||||
s := newTestServer(t, config)
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite))
|
||||
|
||||
response := request(t, s, "POST", "/test-topic/web-push/subscribe", webPushSubscribePayloadExample, map[string]string{
|
||||
"Authorization": util.BasicAuth("ben", "ben"),
|
||||
})
|
||||
|
||||
require.Equal(t, 200, response.Code)
|
||||
require.Equal(t, `{"success":true}`+"\n", response.Body.String())
|
||||
|
||||
subs, err := s.webPushSubscriptionStore.GetSubscriptionsForTopic("test-topic")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
require.Len(t, subs, 1)
|
||||
require.Equal(t, subs[0].Username, "ben")
|
||||
}
|
||||
|
||||
func TestServer_WebPush_TopicSubscribeProtected_Denied(t *testing.T) {
|
||||
config := configureAuth(t, newTestConfigWithWebPush(t))
|
||||
config.AuthDefault = user.PermissionDenyAll
|
||||
s := newTestServer(t, config)
|
||||
|
||||
response := request(t, s, "POST", "/test-topic/web-push/subscribe", webPushSubscribePayloadExample, nil)
|
||||
require.Equal(t, 403, response.Code)
|
||||
|
||||
requireSubscriptionCount(t, s, "test-topic", 0)
|
||||
}
|
||||
|
||||
func TestServer_WebPush_TopicUnsubscribe(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfigWithWebPush(t))
|
||||
|
||||
response := request(t, s, "POST", "/test-topic/web-push/subscribe", webPushSubscribePayloadExample, nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
require.Equal(t, `{"success":true}`+"\n", response.Body.String())
|
||||
|
||||
requireSubscriptionCount(t, s, "test-topic", 1)
|
||||
|
||||
unsubscribe := `{"endpoint":"https://example.com/webpush"}`
|
||||
response = request(t, s, "POST", "/test-topic/web-push/unsubscribe", unsubscribe, nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
require.Equal(t, `{"success":true}`+"\n", response.Body.String())
|
||||
|
||||
requireSubscriptionCount(t, s, "test-topic", 0)
|
||||
}
|
||||
|
||||
func TestServer_WebPush_DeleteAccountUnsubscribe(t *testing.T) {
|
||||
config := configureAuth(t, newTestConfigWithWebPush(t))
|
||||
config.AuthDefault = user.PermissionDenyAll
|
||||
s := newTestServer(t, config)
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite))
|
||||
|
||||
response := request(t, s, "POST", "/test-topic/web-push/subscribe", webPushSubscribePayloadExample, map[string]string{
|
||||
"Authorization": util.BasicAuth("ben", "ben"),
|
||||
})
|
||||
|
||||
require.Equal(t, 200, response.Code)
|
||||
require.Equal(t, `{"success":true}`+"\n", response.Body.String())
|
||||
|
||||
requireSubscriptionCount(t, s, "test-topic", 1)
|
||||
|
||||
request(t, s, "DELETE", "/v1/account", `{"password":"ben"}`, map[string]string{
|
||||
"Authorization": util.BasicAuth("ben", "ben"),
|
||||
})
|
||||
// should've been deleted with the account
|
||||
requireSubscriptionCount(t, s, "test-topic", 0)
|
||||
}
|
||||
|
||||
func TestServer_WebPush_Publish(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfigWithWebPush(t))
|
||||
|
||||
var received atomic.Bool
|
||||
|
||||
upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := io.ReadAll(r.Body)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "/push-receive", r.URL.Path)
|
||||
require.Equal(t, "high", r.Header.Get("Urgency"))
|
||||
require.Equal(t, "", r.Header.Get("Topic"))
|
||||
received.Store(true)
|
||||
}))
|
||||
defer upstreamServer.Close()
|
||||
|
||||
addSubscription(t, s, "test-topic", upstreamServer.URL+"/push-receive")
|
||||
|
||||
request(t, s, "PUT", "/test-topic", "web push test", nil)
|
||||
|
||||
waitFor(t, func() bool {
|
||||
return received.Load()
|
||||
})
|
||||
}
|
||||
|
||||
func TestServer_WebPush_PublishExpire(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfigWithWebPush(t))
|
||||
|
||||
var received atomic.Bool
|
||||
|
||||
upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := io.ReadAll(r.Body)
|
||||
require.Nil(t, err)
|
||||
// Gone
|
||||
w.WriteHeader(410)
|
||||
w.Write([]byte(``))
|
||||
received.Store(true)
|
||||
}))
|
||||
defer upstreamServer.Close()
|
||||
|
||||
addSubscription(t, s, "test-topic", upstreamServer.URL+"/push-receive")
|
||||
addSubscription(t, s, "test-topic-abc", upstreamServer.URL+"/push-receive")
|
||||
|
||||
requireSubscriptionCount(t, s, "test-topic", 1)
|
||||
requireSubscriptionCount(t, s, "test-topic-abc", 1)
|
||||
|
||||
request(t, s, "PUT", "/test-topic", "web push test", nil)
|
||||
|
||||
waitFor(t, func() bool {
|
||||
return received.Load()
|
||||
})
|
||||
|
||||
// Receiving the 410 should've caused the publisher to expire all subscriptions on the endpoint
|
||||
|
||||
requireSubscriptionCount(t, s, "test-topic", 0)
|
||||
requireSubscriptionCount(t, s, "test-topic-abc", 0)
|
||||
}
|
||||
|
||||
func addSubscription(t *testing.T, s *Server, topic string, url string) {
|
||||
err := s.webPushSubscriptionStore.AddSubscription("test-topic", "", webPushSubscribePayload{
|
||||
BrowserSubscription: webpush.Subscription{
|
||||
Endpoint: url,
|
||||
Keys: webpush.Keys{
|
||||
// connected to a local test VAPID key, not a leak!
|
||||
Auth: "kSC3T8aN1JCQxxPdrFLrZg",
|
||||
P256dh: "BMKKbxdUU_xLS7G1Wh5AN8PvWOjCzkCuKZYb8apcqYrDxjOF_2piggBnoJLQYx9IeSD70fNuwawI3e9Y8m3S3PE",
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func requireSubscriptionCount(t *testing.T, s *Server, topic string, expectedLength int) {
|
||||
subs, err := s.webPushSubscriptionStore.GetSubscriptionsForTopic("test-topic")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
require.Len(t, subs, expectedLength)
|
||||
}
|
@ -20,7 +20,7 @@ export const topicUrlJson = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/jso
|
||||
export const topicUrlJsonPoll = (baseUrl, topic) => `${topicUrlJson(baseUrl, topic)}?poll=1`;
|
||||
export const topicUrlJsonPollWithSince = (baseUrl, topic, since) => `${topicUrlJson(baseUrl, topic)}?poll=1&since=${since}`;
|
||||
export const topicUrlAuth = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/auth`;
|
||||
export const topicUrlWebPushSubscribe = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/web-push`;
|
||||
export const topicUrlWebPushSubscribe = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/web-push/subscribe`;
|
||||
export const topicUrlWebPushUnsubscribe = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/web-push/unsubscribe`;
|
||||
export const topicShortUrl = (baseUrl, topic) => shortUrl(topicUrl(baseUrl, topic));
|
||||
export const webPushConfigUrl = (baseUrl) => `${baseUrl}/v1/web-push-config`;
|
||||
|