Klokken i hovedet siger, hvad der er nyt — men kun til den, der kigger på fanen. Det halve af pointen med at få besked er at få den, mens man laver noget andet, og det eneste sted, appen kan sige noget dér, er på sit eget ikon: mærket i Dock'en på en Mac, på proceslinjen på en pc. Sat to steder, fordi der er to tilstande. Appen selv, mens den er åben og kender tallet, og service-workeren, når den ikke er — og det er netop dér, mærket er det, der siger, at der er sket noget. Tallet kommer med i push-beskeden frem for at blive talt op af service-workeren. Den husker ingenting mellem to opvågninger og kunne derfor kun lægge én til noget, den ikke ved hvad er: et mærke, der tæller sig selv op og aldrig kommer ned igen. Serveren ved det i forvejen, og en tælling, der fejler, udelader feltet frem for at sende et nul — så står mærket, som det stod, i stedet for at blive tørret af en optælling, der ikke lykkedes. Ét tal, aldrig en optælling: mærket sættes til hvad der *er*, så det går ned af sig selv, når beskederne bliver læst. Og det ryddes ved log ud — det står i Dock'en, også når ingen er logget ind. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
90 lines
2.7 KiB
Go
90 lines
2.7 KiB
Go
package push
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdh"
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"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"}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|