test(12-03): add discussion SSE coverage
This commit is contained in:
parent
0fa34cebf0
commit
c6dcb680bd
1 changed files with 187 additions and 1 deletions
|
|
@ -7,6 +7,7 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -19,9 +20,13 @@ import (
|
|||
)
|
||||
|
||||
func newDiscussionTestRouter(q *sqlc.Queries, store *auth.Store) http.Handler {
|
||||
return newDiscussionTestRouterWithBroker(q, store, nil)
|
||||
}
|
||||
|
||||
func newDiscussionTestRouterWithBroker(q *sqlc.Queries, store *auth.Store, broker DiscussionRealtime) http.Handler {
|
||||
authDeps := AuthDeps{Queries: q, Store: store, Secure: false}
|
||||
tabloDeps := TablosDeps{Queries: q}
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, TasksDeps{Queries: q}, EtapesDeps{Queries: q}, EventsDeps{Queries: q}, DiscussionDeps{Queries: q}, PlanningDeps{Queries: q}, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
||||
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, TasksDeps{Queries: q}, EtapesDeps{Queries: q}, EventsDeps{Queries: q}, DiscussionDeps{Queries: q, Realtime: broker}, PlanningDeps{Queries: q}, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
||||
if err != nil {
|
||||
panic("newDiscussionTestRouter: " + err.Error())
|
||||
}
|
||||
|
|
@ -286,6 +291,187 @@ func TestDiscussionRequiresCSRF(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
type recordingDiscussionRealtime struct {
|
||||
mu sync.Mutex
|
||||
events []DiscussionEvent
|
||||
}
|
||||
|
||||
func (r *recordingDiscussionRealtime) Subscribe(ctx context.Context, tabloID uuid.UUID) (<-chan DiscussionEvent, func()) {
|
||||
ch := make(chan DiscussionEvent)
|
||||
return ch, func() { close(ch) }
|
||||
}
|
||||
|
||||
func (r *recordingDiscussionRealtime) Publish(event DiscussionEvent) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.events = append(r.events, event)
|
||||
}
|
||||
|
||||
func (r *recordingDiscussionRealtime) lastEvent(t *testing.T) DiscussionEvent {
|
||||
t.Helper()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if len(r.events) == 0 {
|
||||
t.Fatal("no discussion event published")
|
||||
}
|
||||
return r.events[len(r.events)-1]
|
||||
}
|
||||
|
||||
func TestDiscussionStreamRequiresAuth(t *testing.T) {
|
||||
pool, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
q := sqlc.New(pool)
|
||||
store := auth.NewStore(q)
|
||||
router := newDiscussionTestRouter(q, store)
|
||||
|
||||
user := preInsertUser(t, ctx, q, "discussion-stream-owner@example.com", "correct-horse-12")
|
||||
tablo := insertDiscussionTestTablo(t, ctx, q, user, "Stream Auth Tablo")
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/discussion/stream", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusSeeOther {
|
||||
t.Fatalf("unauthenticated stream status = %d; want 303", rec.Code)
|
||||
}
|
||||
if got := rec.Header().Get("Location"); got != "/login" {
|
||||
t.Fatalf("unauthenticated stream Location = %q; want /login", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscussionStreamOwnershipReturns404(t *testing.T) {
|
||||
pool, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
q := sqlc.New(pool)
|
||||
store := auth.NewStore(q)
|
||||
router := newDiscussionTestRouter(q, store)
|
||||
|
||||
owner := preInsertUser(t, ctx, q, "discussion-stream-owner-404@example.com", "correct-horse-12")
|
||||
nonOwner := preInsertUser(t, ctx, q, "discussion-stream-nonowner@example.com", "correct-horse-12")
|
||||
tablo := insertDiscussionTestTablo(t, ctx, q, owner, "Private Stream Tablo")
|
||||
nonOwnerCookie := sessionCookieForUser(t, ctx, store, nonOwner)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/discussion/stream", nil)
|
||||
req.AddCookie(nonOwnerCookie)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Fatalf("non-owner stream status = %d; want 404", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscussionStreamHeaders(t *testing.T) {
|
||||
pool, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
q := sqlc.New(pool)
|
||||
store := auth.NewStore(q)
|
||||
router := newDiscussionTestRouter(q, store)
|
||||
|
||||
user := preInsertUser(t, ctx, q, "discussion-stream-headers@example.com", "correct-horse-12")
|
||||
tablo := insertDiscussionTestTablo(t, ctx, q, user, "Stream Headers Tablo")
|
||||
sessionCookie := sessionCookieForUser(t, ctx, store, user)
|
||||
|
||||
server := httptest.NewServer(router)
|
||||
defer server.Close()
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, server.URL+"/tablos/"+tablo.ID.String()+"/discussion/stream", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("NewRequest: %v", err)
|
||||
}
|
||||
req.AddCookie(sessionCookie)
|
||||
resp, err := server.Client().Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("stream request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("stream status = %d; want 200", resp.StatusCode)
|
||||
}
|
||||
if got := resp.Header.Get("Content-Type"); !strings.HasPrefix(got, "text/event-stream") {
|
||||
t.Fatalf("stream Content-Type = %q; want text/event-stream", got)
|
||||
}
|
||||
if got := resp.Header.Get("Cache-Control"); got != "no-cache" {
|
||||
t.Fatalf("stream Cache-Control = %q; want no-cache", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscussionPostBroadcastsToBroker(t *testing.T) {
|
||||
pool, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
q := sqlc.New(pool)
|
||||
store := auth.NewStore(q)
|
||||
broker := &recordingDiscussionRealtime{}
|
||||
router := newDiscussionTestRouterWithBroker(q, store, broker)
|
||||
|
||||
user := preInsertUser(t, ctx, q, "discussion-broadcast@example.com", "correct-horse-12")
|
||||
tablo := insertDiscussionTestTablo(t, ctx, q, user, "Broadcast Tablo")
|
||||
sessionCookie := sessionCookieForUser(t, ctx, store, user)
|
||||
csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/"+tablo.ID.String()+"/discussion", []*http.Cookie{sessionCookie})
|
||||
|
||||
form := url.Values{"body": {"Broadcast me"}, "_csrf": {csrfToken}}
|
||||
req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/discussion/messages", strings.NewReader(form.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("HX-Request", "true")
|
||||
for _, c := range csrfCookies {
|
||||
req.AddCookie(c)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("POST discussion message status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
|
||||
}
|
||||
event := broker.lastEvent(t)
|
||||
if event.TabloID != tablo.ID {
|
||||
t.Fatalf("published TabloID = %s; want %s", event.TabloID, tablo.ID)
|
||||
}
|
||||
if event.MessageID == uuid.Nil {
|
||||
t.Fatal("published event missing message ID")
|
||||
}
|
||||
if event.AuthorUserID != user.ID {
|
||||
t.Fatalf("published AuthorUserID = %s; want %s", event.AuthorUserID, user.ID)
|
||||
}
|
||||
if !strings.Contains(event.MessageHTML, "Broadcast me") {
|
||||
t.Fatalf("published message HTML missing body: %.500s", event.MessageHTML)
|
||||
}
|
||||
if !event.RefreshUnread {
|
||||
t.Fatal("published event did not request unread refresh")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscussionBrokerUnregistersOnCancel(t *testing.T) {
|
||||
broker := NewDiscussionBroker()
|
||||
tabloID := uuid.New()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
_, unsubscribe := broker.Subscribe(ctx, tabloID)
|
||||
if got := broker.SubscriberCount(tabloID); got != 1 {
|
||||
t.Fatalf("subscriber count after subscribe = %d; want 1", got)
|
||||
}
|
||||
cancel()
|
||||
|
||||
deadline := time.After(500 * time.Millisecond)
|
||||
for broker.SubscriberCount(tabloID) != 0 {
|
||||
select {
|
||||
case <-deadline:
|
||||
unsubscribe()
|
||||
t.Fatalf("subscriber count after cancel = %d; want 0", broker.SubscriberCount(tabloID))
|
||||
default:
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTablosListDiscussionUnreadBadge(t *testing.T) {
|
||||
pool, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
|
|
|||
Loading…
Reference in a new issue