1
0
Fork 0
mirror of https://github.com/imjasonh/webpush synced 2026-07-06 23:52:25 +00:00
webpush/integration_test.go
copilot-swe-agent[bot] 78bd40dc88 Implement Web Push API Go library with VAPID support and pluggable storage
Co-authored-by: imjasonh <210737+imjasonh@users.noreply.github.com>
2025-11-24 21:19:16 +00:00

241 lines
6.3 KiB
Go

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))
}
}