mirror of
https://github.com/imjasonh/webpush
synced 2026-07-10 17:22:02 +00:00
Implement Web Push API Go library with VAPID support and pluggable storage
Co-authored-by: imjasonh <210737+imjasonh@users.noreply.github.com>
This commit is contained in:
parent
7d0082c045
commit
78bd40dc88
15 changed files with 2708 additions and 1 deletions
241
integration_test.go
Normal file
241
integration_test.go
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
package webpush_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/imjasonh/webpush"
|
||||
"github.com/imjasonh/webpush/keys"
|
||||
"github.com/imjasonh/webpush/storage"
|
||||
)
|
||||
|
||||
// TestIntegration_FullFlow tests the complete flow of generating keys,
|
||||
// storing subscriptions, and sending push notifications.
|
||||
func TestIntegration_FullFlow(t *testing.T) {
|
||||
// 1. Generate VAPID keys
|
||||
privateKeyB64, publicKeyB64, err := keys.GenerateKeyPair()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKeyPair() error = %v", err)
|
||||
}
|
||||
t.Logf("Generated public key: %s", publicKeyB64)
|
||||
|
||||
// 2. Create signer from generated key
|
||||
signer, err := keys.NewFileSignerFromBase64(privateKeyB64)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFileSignerFromBase64() error = %v", err)
|
||||
}
|
||||
|
||||
// Verify public keys match
|
||||
if signer.PublicKeyBase64() != publicKeyB64 {
|
||||
t.Errorf("Public keys don't match")
|
||||
}
|
||||
|
||||
// 3. Create storage
|
||||
store := storage.NewMemory()
|
||||
|
||||
// 4. Create a mock push service
|
||||
pushReceived := make(chan struct{}, 1)
|
||||
pushServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Verify headers
|
||||
if r.Header.Get("Authorization") == "" {
|
||||
t.Error("Missing Authorization header")
|
||||
}
|
||||
if r.Header.Get("Content-Encoding") != "aes128gcm" {
|
||||
t.Errorf("Content-Encoding = %q, want %q", r.Header.Get("Content-Encoding"), "aes128gcm")
|
||||
}
|
||||
if r.Header.Get("TTL") == "" {
|
||||
t.Error("Missing TTL header")
|
||||
}
|
||||
|
||||
// Read body to verify it's encrypted
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
if len(body) < 86 { // Minimum header size
|
||||
t.Errorf("Body too short: %d bytes", len(body))
|
||||
}
|
||||
|
||||
pushReceived <- struct{}{}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}))
|
||||
defer pushServer.Close()
|
||||
|
||||
// 5. Simulate client subscription
|
||||
// In real world, this would come from the browser's PushManager.subscribe()
|
||||
clientSubJSON := []byte(`{
|
||||
"endpoint": "` + pushServer.URL + `/push/abc123",
|
||||
"keys": {
|
||||
"p256dh": "BNcRdreALRFXTkOOUHK1EtK2wtaz5Ry4YfYCA_0QTpQtUbVlUls0VJXg7A8u-Ts1XbjhazAkj7I99e8QcYP7DkM",
|
||||
"auth": "tBHItJI5svbpez7KI4CCXg"
|
||||
}
|
||||
}`)
|
||||
|
||||
sub, err := webpush.ParseSubscription(clientSubJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSubscription() error = %v", err)
|
||||
}
|
||||
|
||||
// 6. Store the subscription
|
||||
record := &storage.Record{
|
||||
ID: "sub-1",
|
||||
UserID: "user-123",
|
||||
Subscription: sub,
|
||||
}
|
||||
if err := store.Save(context.Background(), record); err != nil {
|
||||
t.Fatalf("Save() error = %v", err)
|
||||
}
|
||||
|
||||
// 7. Retrieve subscriptions for user
|
||||
records, err := store.GetByUserID(context.Background(), "user-123")
|
||||
if err != nil {
|
||||
t.Fatalf("GetByUserID() error = %v", err)
|
||||
}
|
||||
if len(records) != 1 {
|
||||
t.Fatalf("GetByUserID() count = %d, want 1", len(records))
|
||||
}
|
||||
|
||||
// 8. Create web push client and send notification
|
||||
client := webpush.NewClient(signer, "mailto:test@example.com")
|
||||
client.WithHTTPClient(pushServer.Client())
|
||||
|
||||
payload := map[string]string{
|
||||
"title": "Hello",
|
||||
"body": "World",
|
||||
}
|
||||
payloadJSON, _ := json.Marshal(payload)
|
||||
|
||||
err = client.Send(context.Background(), records[0].Subscription, payloadJSON, &webpush.Options{
|
||||
TTL: 3600,
|
||||
Urgency: "high",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Send() error = %v", err)
|
||||
}
|
||||
|
||||
// 9. Verify push was received
|
||||
select {
|
||||
case <-pushReceived:
|
||||
t.Log("Push notification received by mock service")
|
||||
default:
|
||||
t.Error("Push notification not received")
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegration_MultipleSubscriptions tests sending to multiple subscriptions.
|
||||
func TestIntegration_MultipleSubscriptions(t *testing.T) {
|
||||
// Generate keys
|
||||
privateKeyB64, _, err := keys.GenerateKeyPair()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKeyPair() error = %v", err)
|
||||
}
|
||||
|
||||
signer, err := keys.NewFileSignerFromBase64(privateKeyB64)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFileSignerFromBase64() error = %v", err)
|
||||
}
|
||||
|
||||
// Track received pushes
|
||||
receivedCount := 0
|
||||
pushServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
receivedCount++
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}))
|
||||
defer pushServer.Close()
|
||||
|
||||
store := storage.NewMemory()
|
||||
ctx := context.Background()
|
||||
|
||||
// Create multiple subscriptions for same user
|
||||
for i := 0; i < 3; i++ {
|
||||
record := &storage.Record{
|
||||
ID: "sub-" + string(rune('a'+i)),
|
||||
UserID: "user-1",
|
||||
Subscription: &webpush.Subscription{
|
||||
Endpoint: pushServer.URL + "/push/" + string(rune('a'+i)),
|
||||
Keys: webpush.Keys{
|
||||
P256dh: "BNcRdreALRFXTkOOUHK1EtK2wtaz5Ry4YfYCA_0QTpQtUbVlUls0VJXg7A8u-Ts1XbjhazAkj7I99e8QcYP7DkM",
|
||||
Auth: "tBHItJI5svbpez7KI4CCXg",
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := store.Save(ctx, record); err != nil {
|
||||
t.Fatalf("Save() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Send to all user's subscriptions
|
||||
client := webpush.NewClient(signer, "mailto:test@example.com")
|
||||
client.WithHTTPClient(pushServer.Client())
|
||||
|
||||
records, err := store.GetByUserID(ctx, "user-1")
|
||||
if err != nil {
|
||||
t.Fatalf("GetByUserID() error = %v", err)
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
err := client.Send(ctx, record.Subscription, []byte(`{"test":true}`), nil)
|
||||
if err != nil {
|
||||
t.Errorf("Send() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if receivedCount != 3 {
|
||||
t.Errorf("Received %d pushes, want 3", receivedCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegration_SQLiteStorage tests the SQLite storage backend.
|
||||
func TestIntegration_SQLiteStorage(t *testing.T) {
|
||||
store, err := storage.NewSQLite(":memory:")
|
||||
if err != nil {
|
||||
t.Fatalf("NewSQLite() error = %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Save multiple subscriptions
|
||||
for i := 0; i < 5; i++ {
|
||||
record := &storage.Record{
|
||||
ID: "sub-" + string(rune('a'+i)),
|
||||
UserID: "user-1",
|
||||
Subscription: &webpush.Subscription{
|
||||
Endpoint: "https://push.example.com/" + string(rune('a'+i)),
|
||||
Keys: webpush.Keys{
|
||||
P256dh: "key-" + string(rune('a'+i)),
|
||||
Auth: "auth-" + string(rune('a'+i)),
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := store.Save(ctx, record); err != nil {
|
||||
t.Fatalf("Save() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// List with pagination
|
||||
page1, err := store.List(ctx, 2, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("List() error = %v", err)
|
||||
}
|
||||
if len(page1) != 2 {
|
||||
t.Errorf("Page 1 len = %d, want 2", len(page1))
|
||||
}
|
||||
|
||||
page2, err := store.List(ctx, 2, 2)
|
||||
if err != nil {
|
||||
t.Fatalf("List() error = %v", err)
|
||||
}
|
||||
if len(page2) != 2 {
|
||||
t.Errorf("Page 2 len = %d, want 2", len(page2))
|
||||
}
|
||||
|
||||
page3, err := store.List(ctx, 2, 4)
|
||||
if err != nil {
|
||||
t.Fatalf("List() error = %v", err)
|
||||
}
|
||||
if len(page3) != 1 {
|
||||
t.Errorf("Page 3 len = %d, want 1", len(page3))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue