eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1package apns
2
3import (
4 "crypto/ecdsa"
5 "crypto/elliptic"
6 "crypto/rand"
7 "crypto/x509"
8 "encoding/pem"
9 "errors"
10 "net/http"
11 "net/http/httptest"
12 "strings"
13 "testing"
14)
15
16// testKeyPEM returns a throwaway P-256 key as PKCS8 PEM, the shape apns.New
17// expects for a real .p8.
18func testKeyPEM(t *testing.T) string {
19 t.Helper()
20 key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
21 if err != nil {
22 t.Fatal(err)
23 }
24 der, err := x509.MarshalPKCS8PrivateKey(key)
25 if err != nil {
26 t.Fatal(err)
27 }
28 return string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der}))
29}
30
31func newTestClient(t *testing.T, stubURL string) *Client {
32 t.Helper()
33 c, err := New(testKeyPEM(t), "KID1234567", "TEAM123456", "com.example.app")
34 if err != nil {
35 t.Fatalf("New: %v", err)
36 }
37 c.SetHosts(stubURL, stubURL)
38 return c
39}
40
41func TestValidDeviceToken(t *testing.T) {
42 ok := []string{"a", "AABBCCDD", strings.Repeat("f", 200)}
43 for _, s := range ok {
44 if !ValidDeviceToken(s) {
45 t.Errorf("expected %q valid", s)
46 }
47 }
48 bad := []string{"", "xyz", "aa/bb", "aa..bb", "aa\r\nHost:", strings.Repeat("a", 201), "zz"}
49 for _, s := range bad {
50 if ValidDeviceToken(s) {
51 t.Errorf("expected %q invalid", s)
52 }
53 }
54}
55
56// TestBuildPayloadStable pins the aps envelope byte-for-byte. The iOS
57// Notification Service Extension mirrors these exact keys; if this changes,
58// on-device decryption breaks — so a drift must be a deliberate, reviewed edit.
59func TestBuildPayloadStable(t *testing.T) {
60 got := string(BuildPayload("t1", "ciphertext-blob", 0, 0))
61 want := `{"aps":{"alert":{"title":"New message"},"mutable-content":1,"sound":"default","thread-id":"t1"},"blob":"ciphertext-blob","thread":"t1"}`
62 if got != want {
63 t.Errorf("envelope drift:\n got: %s\nwant: %s", got, want)
64 }
65 // With a message id/created_at the payload carries them (the NSE's local
66 // store ingest); id 0 (test pushes, old relays) must omit both.
67 got = string(BuildPayload("t1", "ciphertext-blob", 42, 1700000000))
68 want = `{"aps":{"alert":{"title":"New message"},"mutable-content":1,"sound":"default","thread-id":"t1"},"at":1700000000,"blob":"ciphertext-blob","id":42,"thread":"t1"}`
69 if got != want {
70 t.Errorf("envelope drift (id/at):\n got: %s\nwant: %s", got, want)
71 }
72}
73
74func TestSendClassifiesResponses(t *testing.T) {
75 const (
76 live = "aaaaaaaa"
77 dead = "deadbeef"
78 bad = "badd0000"
79 err5 = "cccccccc"
80 )
81 stub := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
82 switch {
83 case strings.Contains(r.URL.Path, dead):
84 w.WriteHeader(http.StatusGone)
85 case strings.Contains(r.URL.Path, bad):
86 w.WriteHeader(http.StatusBadRequest)
87 w.Write([]byte(`{"reason":"BadDeviceToken"}`))
88 case strings.Contains(r.URL.Path, err5):
89 w.WriteHeader(http.StatusInternalServerError)
90 w.Write([]byte(`{"reason":"InternalServerError"}`))
91 default:
92 w.WriteHeader(http.StatusOK)
93 }
94 }))
95 defer stub.Close()
96 c := newTestClient(t, stub.URL)
97
98 if err := c.Send(live, false, "th", "blob", 1, 1); err != nil {
99 t.Errorf("live token: want nil, got %v", err)
100 }
101 if err := c.Send(dead, false, "th", "blob", 1, 1); !errors.Is(err, ErrGone) {
102 t.Errorf("410: want ErrGone, got %v", err)
103 }
104 if err := c.Send(bad, false, "th", "blob", 1, 1); !errors.Is(err, ErrGone) {
105 t.Errorf("BadDeviceToken: want ErrGone, got %v", err)
106 }
107 if err := c.Send(err5, false, "th", "blob", 1, 1); err == nil || errors.Is(err, ErrGone) {
108 t.Errorf("500: want a non-gone error, got %v", err)
109 }
110}
111
112// A non-hex token must be rejected before it can reach the URL path, and must
113// never be classified as gone.
114func TestSendRejectsBadToken(t *testing.T) {
115 reached := false
116 stub := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { reached = true }))
117 defer stub.Close()
118 c := newTestClient(t, stub.URL)
119 err := c.Send("aa/../bb", false, "th", "blob", 1, 1)
120 if err == nil || errors.Is(err, ErrGone) {
121 t.Errorf("bad token: want a non-gone error, got %v", err)
122 }
123 if reached {
124 t.Error("bad token reached the upstream server — validation is too late")
125 }
126}
127
128func TestBuildAlertPayloadStable(t *testing.T) {
129 // The plaintext changelog banner (announce.go): no mutable-content (the NSE
130 // never runs — the words are already readable), a `changelog` marker for tap
131 // routing. Pinned like BuildPayload so the messenger and relay can't drift.
132 got := string(BuildAlertPayload("What's new in Test Space", "Updates take seconds."))
133 want := `{"aps":{"alert":{"body":"Updates take seconds.","title":"What's new in Test Space"},"sound":"default"},"changelog":true}`
134 if got != want {
135 t.Errorf("alert envelope drift:\n got: %s\nwant: %s", got, want)
136 }
137}
138