2026-08-20 15:15:28 +02:00
|
|
|
package push
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"crypto/ecdh"
|
|
|
|
|
"crypto/rand"
|
2026-09-08 12:12:54 +02:00
|
|
|
"encoding/json"
|
2026-08-20 15:15:28 +02:00
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// A subscription must not be able to point the server at its own network.
|
|
|
|
|
//
|
|
|
|
|
// The endpoint is whatever the browser handed over, which is to say whatever the
|
|
|
|
|
// person signed in handed over, and this server POSTs to it on a schedule they
|
|
|
|
|
// choose. The subscribe handler refuses a private address when it is written
|
|
|
|
|
// down — and that check runs on the URL as written, so a name it approves can
|
|
|
|
|
// answer 127.0.0.1 the next time it is looked up. This is the other half: the
|
|
|
|
|
// address that will actually be dialled, checked at the moment of dialling.
|
|
|
|
|
func TestSendRefusesToReachInside(t *testing.T) {
|
|
|
|
|
reached := false
|
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
reached = true
|
|
|
|
|
w.WriteHeader(http.StatusCreated)
|
|
|
|
|
}))
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
// httptest listens on 127.0.0.1, which is exactly the destination in question.
|
|
|
|
|
if !strings.Contains(srv.URL, "127.0.0.1") {
|
|
|
|
|
t.Skipf("the test server is not on loopback: %s", srv.URL)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err := Send(context.Background(), testSubscription(t, srv.URL), Payload{Title: "hej"}, testVAPID(t))
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("Send reached a loopback address without complaining")
|
|
|
|
|
}
|
|
|
|
|
if reached {
|
|
|
|
|
t.Error("the request arrived: the wall let it through and only the answer failed")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testSubscription(t *testing.T, endpoint string) Subscription {
|
|
|
|
|
t.Helper()
|
|
|
|
|
key, err := ecdh.P256().GenerateKey(rand.Reader)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
auth := make([]byte, 16)
|
|
|
|
|
if _, err := rand.Read(auth); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
return Subscription{
|
|
|
|
|
Endpoint: endpoint,
|
|
|
|
|
P256dh: b64.EncodeToString(key.PublicKey().Bytes()),
|
|
|
|
|
Auth: b64.EncodeToString(auth),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testVAPID(t *testing.T) VAPID {
|
|
|
|
|
t.Helper()
|
|
|
|
|
pub, priv, err := GenerateVAPIDKeys()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
return VAPID{Public: pub, Private: priv, Subject: "mailto:kristian@example.dk"}
|
|
|
|
|
}
|
2026-09-08 12:12:54 +02:00
|
|
|
|
|
|
|
|
// Tallet til mærket på ikonet skal med i beskeden — og skal blive væk, når der
|
|
|
|
|
// ikke er noget at sige. Service-workeren læser netop det: er `unread` ikke et
|
|
|
|
|
// tal, står mærket, som det stod, frem for at blive nulstillet af en tælling, der
|
|
|
|
|
// ikke lykkedes.
|
|
|
|
|
func TestPayloadCarriesTheUnreadCount(t *testing.T) {
|
|
|
|
|
with, err := json.Marshal(Payload{Title: "hej", Unread: 3})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(string(with), `"unread":3`) {
|
|
|
|
|
t.Errorf("payload = %s, want the unread count in it", with)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
without, err := json.Marshal(Payload{Title: "hej"})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if strings.Contains(string(without), "unread") {
|
|
|
|
|
t.Errorf("payload = %s, want no count when there is none to send", without)
|
|
|
|
|
}
|
|
|
|
|
}
|