package api import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "sync" "testing" "github.com/thepeterstone/claudomator/internal/storage" ) // mockPushStore implements pushSubscriptionStore for testing. type mockPushStore struct { mu sync.Mutex subs []storage.PushSubscription } func (m *mockPushStore) SavePushSubscription(sub storage.PushSubscription) error { m.mu.Lock() defer m.mu.Unlock() // Upsert by endpoint. for i, s := range m.subs { if s.Endpoint == sub.Endpoint { m.subs[i] = sub return nil } } m.subs = append(m.subs, sub) return nil } func (m *mockPushStore) DeletePushSubscription(endpoint string) error { m.mu.Lock() defer m.mu.Unlock() filtered := m.subs[:0] for _, s := range m.subs { if s.Endpoint != endpoint { filtered = append(filtered, s) } } m.subs = filtered return nil } func (m *mockPushStore) ListPushSubscriptions() ([]storage.PushSubscription, error) { m.mu.Lock() defer m.mu.Unlock() cp := make([]storage.PushSubscription, len(m.subs)) copy(cp, m.subs) return cp, nil } func testServerWithPush(t *testing.T) (*Server, *mockPushStore) { t.Helper() srv, _ := testServer(t) ps := &mockPushStore{} srv.SetVAPIDConfig("testpub", "testpriv", "mailto:test@example.com") srv.SetPushStore(ps) return srv, ps } func TestHandleGetVAPIDKey(t *testing.T) { srv, _ := testServerWithPush(t) req := httptest.NewRequest("GET", "/api/push/vapid-key", nil) rec := httptest.NewRecorder() srv.mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("want 200, got %d", rec.Code) } var resp map[string]string if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode response: %v", err) } if resp["public_key"] != "testpub" { t.Errorf("want public_key %q, got %q", "testpub", resp["public_key"]) } } func TestHandlePushSubscribe_CreatesSub(t *testing.T) { srv, ps := testServerWithPush(t) body := `{"endpoint":"https://push.example.com/sub1","keys":{"p256dh":"key1","auth":"auth1"}}` req := httptest.NewRequest("POST", "/api/push/subscribe", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() srv.mux.ServeHTTP(rec, req) if rec.Code != http.StatusCreated { t.Fatalf("want 201, got %d: %s", rec.Code, rec.Body.String()) } subs, _ := ps.ListPushSubscriptions() if len(subs) != 1 { t.Fatalf("want 1 subscription, got %d", len(subs)) } if subs[0].Endpoint != "https://push.example.com/sub1" { t.Errorf("endpoint: want %q, got %q", "https://push.example.com/sub1", subs[0].Endpoint) } } func TestHandlePushSubscribe_MissingEndpoint(t *testing.T) { srv, _ := testServerWithPush(t) body := `{"keys":{"p256dh":"key1","auth":"auth1"}}` req := httptest.NewRequest("POST", "/api/push/subscribe", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() srv.mux.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("want 400, got %d", rec.Code) } } func TestHandlePushUnsubscribe_DeletesSub(t *testing.T) { srv, ps := testServerWithPush(t) // Add a subscription. ps.SavePushSubscription(storage.PushSubscription{ //nolint:errcheck ID: "sub-1", Endpoint: "https://push.example.com/todelete", P256DHKey: "key", AuthKey: "auth", }) body := `{"endpoint":"https://push.example.com/todelete"}` req := httptest.NewRequest("DELETE", "/api/push/subscribe", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() srv.mux.ServeHTTP(rec, req) if rec.Code != http.StatusNoContent { t.Fatalf("want 204, got %d: %s", rec.Code, rec.Body.String()) } subs, _ := ps.ListPushSubscriptions() if len(subs) != 0 { t.Errorf("want 0 subscriptions after delete, got %d", len(subs)) } } func TestHandlePushUnsubscribe_MissingEndpoint(t *testing.T) { srv, _ := testServerWithPush(t) body := `{}` req := httptest.NewRequest("DELETE", "/api/push/subscribe", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() srv.mux.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("want 400, got %d", rec.Code) } }