| index bdb0fab..601913b 100644 |
| --- a/http_test.go |
| +++ b/http_test.go |
| @@ -108,10 +108,32 @@ func TestSubscribeGating(t *testing.T) { |
| if w := post(t, s.Subscribe, "{not json", "alice", true); w.Code != http.StatusBadRequest { |
| t.Errorf("bad json: %d", w.Code) |
| } |
| + for name, tail := range map[string]string{"garbage": "garbage", "second doc": `{"x":1}`, "stray brace": "}"} { |
| + if w := post(t, s.Subscribe, body+tail, "alice", true); w.Code != http.StatusBadRequest { |
| + t.Errorf("trailing %s: %d", name, w.Code) |
| + } |
| + } |
| huge := `{"subscription":{"endpoint":"https://push.example/` + strings.Repeat("x", 8193) + `"}}` |
| if w := post(t, s.Subscribe, huge, "alice", true); w.Code != http.StatusRequestEntityTooLarge { |
| t.Errorf("oversize body: %d", w.Code) |
| } |
| + // A valid body padded with whitespace to exactly the cap passes; |
| + // one byte over does not. |
| + exact := body + strings.Repeat(" ", 8192-len(body)) |
| + if w := post(t, s.Subscribe, exact, "alice", true); w.Code != http.StatusNoContent { |
| + t.Errorf("8192-byte body: %d", w.Code) |
| + } |
| + if w := post(t, s.Subscribe, exact+" ", "alice", true); w.Code != http.StatusRequestEntityTooLarge { |
| + t.Errorf("8193-byte body: %d", w.Code) |
| + } |
| + // Endpoint length: 2048 accepted, 2049 refused. |
| + prefix := "https://push.example/" |
| + if w := post(t, s.Subscribe, subscribeBody(s, prefix+strings.Repeat("e", 2048-len(prefix))), "alice", true); w.Code != http.StatusNoContent { |
| + t.Errorf("2048-byte endpoint: %d", w.Code) |
| + } |
| + if w := post(t, s.Subscribe, subscribeBody(s, prefix+strings.Repeat("e", 2049-len(prefix))), "alice", true); w.Code != http.StatusBadRequest { |
| + t.Errorf("2049-byte endpoint: %d", w.Code) |
| + } |
| r := httptest.NewRequest(http.MethodGet, "/aviso/subscribe", nil) |
| w := httptest.NewRecorder() |
| s.Subscribe(w, r) |
| @@ -120,6 +142,57 @@ func TestSubscribeGating(t *testing.T) { |
| } |
| } |
| |
| +// Bad keys are refused before put, so a mangled re-subscribe can |
| +// neither replace working keys nor delete previousEndpoint. |
| +func TestSubscribeRefusesInvalidKeysWithoutTouchingRows(t *testing.T) { |
| + s := newService(t) |
| + ctx := context.Background() |
| + _ = post(t, s.Subscribe, subscribeBody(s, "https://push.example/old"), "alice", true) |
| + _ = post(t, s.Subscribe, subscribeBody(s, "https://push.example/e1"), "alice", true) |
| + before, _ := s.List(ctx, "alice") |
| + good := keys() |
| + for name, k := range map[string]map[string]string{ |
| + "p256dh not base64": {"p256dh": "!", "auth": good["auth"]}, |
| + "p256dh not a point": {"p256dh": base64.RawURLEncoding.EncodeToString(make([]byte, 65)), "auth": good["auth"]}, |
| + "p256dh compressed": {"p256dh": good["p256dh"][:44], "auth": good["auth"]}, |
| + "auth short": {"p256dh": good["p256dh"], "auth": base64.RawURLEncoding.EncodeToString(make([]byte, 15))}, |
| + "auth not base64": {"p256dh": good["p256dh"], "auth": "!"}, |
| + } { |
| + b, _ := json.Marshal(map[string]any{ |
| + "subscription": map[string]any{"endpoint": "https://push.example/e1", "keys": k}, |
| + "publicKey": s.PublicKeyString(), |
| + "previousEndpoint": "https://push.example/old", |
| + }) |
| + if w := post(t, s.Subscribe, string(b), "alice", true); w.Code != http.StatusBadRequest { |
| + t.Errorf("%s: %d", name, w.Code) |
| + } |
| + } |
| + after, _ := s.List(ctx, "alice") |
| + if len(after) != 2 || after[0].Auth != before[0].Auth || after[1].Auth != before[1].Auth || after[1].Revision != before[1].Revision { |
| + t.Fatalf("rows changed by refused requests:\n%+v\n%+v", before, after) |
| + } |
| + // Padded base64url, which some toJSON() implementations emit, is fine. |
| + padded := map[string]string{ |
| + "p256dh": base64.URLEncoding.EncodeToString(mustDecode(good["p256dh"])), |
| + "auth": base64.URLEncoding.EncodeToString(mustDecode(good["auth"])), |
| + } |
| + b, _ := json.Marshal(map[string]any{ |
| + "subscription": map[string]any{"endpoint": "https://push.example/e2", "keys": padded}, |
| + "publicKey": s.PublicKeyString(), |
| + }) |
| + if w := post(t, s.Subscribe, string(b), "alice", true); w.Code != http.StatusNoContent { |
| + t.Fatalf("padded keys: %d %s", w.Code, w.Body) |
| + } |
| +} |
| + |
| +func mustDecode(s string) []byte { |
| + b, err := base64.RawURLEncoding.DecodeString(s) |
| + if err != nil { |
| + panic(err) |
| + } |
| + return b |
| +} |
| + |
| // Real browsers' toJSON() carries expirationTime; a strict decoder |
| // would refuse every genuine subscription. |
| func TestSubscribeToleratesBrowserFields(t *testing.T) { |
| @@ -156,6 +229,20 @@ func TestSubscribeHonoursPreviousEndpoint(t *testing.T) { |
| if w := post(t, s.Subscribe, string(b), "alice", true); w.Code != http.StatusBadRequest { |
| t.Fatalf("bad previousEndpoint accepted: %d", w.Code) |
| } |
| + // Naming another subject's endpoint as previous is a 204 that |
| + // deletes nothing of theirs. |
| + _ = post(t, s.Subscribe, subscribeBody(s, "https://push.example/bobs"), "bob", true) |
| + b, _ = json.Marshal(map[string]any{ |
| + "subscription": map[string]any{"endpoint": "https://push.example/new3", "keys": keys()}, |
| + "publicKey": s.PublicKeyString(), |
| + "previousEndpoint": "https://push.example/bobs", |
| + }) |
| + if w := post(t, s.Subscribe, string(b), "alice", true); w.Code != http.StatusNoContent { |
| + t.Fatalf("%d %s", w.Code, w.Body) |
| + } |
| + if rows, _ := s.List(context.Background(), "bob"); len(rows) != 1 { |
| + t.Fatal("alice's previousEndpoint deleted bob's row") |
| + } |
| } |
| |
| func TestUnsubscribe(t *testing.T) { |
| @@ -178,6 +265,12 @@ func TestUnsubscribe(t *testing.T) { |
| if w := post(t, s.Unsubscribe, `{}`, "alice", true); w.Code != http.StatusBadRequest { |
| t.Errorf("missing endpoint: %d", w.Code) |
| } |
| + if w := post(t, s.Unsubscribe, body+"garbage", "alice", true); w.Code != http.StatusBadRequest { |
| + t.Errorf("trailing garbage: %d", w.Code) |
| + } |
| + if rows, _ := s.List(ctx, "alice"); len(rows) != 1 { |
| + t.Fatal("a refused unsubscribe removed the row") |
| + } |
| if w := post(t, s.Unsubscribe, body, "alice", true); w.Code != http.StatusNoContent { |
| t.Errorf("own: %d", w.Code) |
| } |