Files
spsays-bff/internal/service/broadcast_service_test.go
T
2026-08-29 18:34:07 +05:30

167 lines
4.8 KiB
Go

package service
import (
"context"
"os"
"path/filepath"
"testing"
"time"
"github.com/google/uuid"
"github.com/joho/godotenv"
"github.com/prabhupadasays/spsays-bff/internal/repository"
"go.uber.org/zap"
)
func testBroadcastService(t *testing.T) (*BroadcastService, *repository.Postgres) {
t.Helper()
uri := os.Getenv("POSTGRES_URI")
if uri == "" {
_ = godotenv.Load(filepath.Join("..", "..", ".env"))
uri = os.Getenv("POSTGRES_URI")
}
if uri == "" {
t.Skip("POSTGRES_URI not set")
}
pg, err := repository.NewPostgres(context.Background(), uri, zap.NewNop())
if err != nil {
t.Skipf("postgres unreachable: %v", err)
}
t.Cleanup(pg.Close)
// Nil push/realtime/R2: these tests are about who sees what, and none of
// those services are reachable from a test run.
return NewBroadcastService(repository.NewBroadcastRepo(pg), nil, nil, nil, nil, nil, zap.NewNop()), pg
}
func bcUser(t *testing.T, pg *repository.Postgres, label string) string {
t.Helper()
id := uuid.NewString()
suffix := id[:8]
if _, err := pg.Pool.Exec(context.Background(), `
INSERT INTO users (id, email, password_hash, name, handle)
VALUES ($1, $2, 'x', $3, $4)`,
id, "bc-"+suffix+"@example.invalid", "BC "+label, "bc_"+suffix); err != nil {
t.Fatalf("create user: %v", err)
}
t.Cleanup(func() {
_, _ = pg.Pool.Exec(context.Background(), `DELETE FROM users WHERE id = $1`, id)
})
return id
}
func cleanupBroadcast(t *testing.T, pg *repository.Postgres, id string) {
t.Cleanup(func() {
_, _ = pg.Pool.Exec(context.Background(), `DELETE FROM broadcasts WHERE id = $1`, id)
})
}
// "At least and at most once" is the whole contract. A user must be offered a
// published announcement until they dismiss it, and never again after.
func TestBroadcast_ShownExactlyOncePerUser(t *testing.T) {
svc, pg := testBroadcastService(t)
ctx := context.Background()
admin, alice, bob := bcUser(t, pg, "Admin"), bcUser(t, pg, "A"), bcUser(t, pg, "B")
b, err := svc.Create(ctx, admin, CreateParams{
Title: "New: chat", Body: "Direct messages are here.",
})
if err != nil {
t.Fatal(err)
}
cleanupBroadcast(t, pg, b.ID)
// A draft is nobody's business yet.
if got, _ := svc.Pending(ctx, alice); got != nil {
t.Fatalf("an unpublished draft was offered: %+v", got)
}
if _, err := svc.Publish(ctx, b.ID); err != nil {
t.Fatal(err)
}
// At least once: offered, and still offered until dismissed.
for i := 0; i < 2; i++ {
got, err := svc.Pending(ctx, alice)
if err != nil {
t.Fatal(err)
}
if got == nil || got.ID != b.ID {
t.Fatalf("call %d: expected the published announcement, got %+v", i+1, got)
}
}
// At most once: dismissed means gone, and dismissing twice is harmless.
if err := svc.MarkSeen(ctx, b.ID, alice); err != nil {
t.Fatal(err)
}
if err := svc.MarkSeen(ctx, b.ID, alice); err != nil {
t.Errorf("second dismissal should be idempotent, got %v", err)
}
if got, _ := svc.Pending(ctx, alice); got != nil {
t.Errorf("shown again after dismissal: %+v", got)
}
// One person dismissing must not dismiss it for everyone.
if got, _ := svc.Pending(ctx, bob); got == nil || got.ID != b.ID {
t.Errorf("another user lost the announcement: %+v", got)
}
}
// An announcement past its expiry stops being offered, so someone signing up
// months later isn't shown last quarter's news.
func TestBroadcast_ExpiryStopsDelivery(t *testing.T) {
svc, pg := testBroadcastService(t)
ctx := context.Background()
admin, reader := bcUser(t, pg, "Admin"), bcUser(t, pg, "R")
past := time.Now().Add(-time.Hour)
b, err := svc.Create(ctx, admin, CreateParams{
Title: "Old news", Body: "Yesterday's announcement.", ExpiresAt: &past,
})
if err != nil {
t.Fatal(err)
}
cleanupBroadcast(t, pg, b.ID)
if _, err := svc.Publish(ctx, b.ID); err != nil {
t.Fatal(err)
}
if got, _ := svc.Pending(ctx, reader); got != nil {
t.Errorf("an expired announcement was offered: %+v", got)
}
}
// Only the newest announcement is offered: two modals stacked on launch is a
// worse experience than missing the older one.
func TestBroadcast_OffersNewestFirst(t *testing.T) {
svc, pg := testBroadcastService(t)
ctx := context.Background()
admin, reader := bcUser(t, pg, "Admin"), bcUser(t, pg, "R")
older, err := svc.Create(ctx, admin, CreateParams{Title: "First", Body: "one"})
if err != nil {
t.Fatal(err)
}
cleanupBroadcast(t, pg, older.ID)
if _, err := svc.Publish(ctx, older.ID); err != nil {
t.Fatal(err)
}
newer, err := svc.Create(ctx, admin, CreateParams{Title: "Second", Body: "two"})
if err != nil {
t.Fatal(err)
}
cleanupBroadcast(t, pg, newer.ID)
if _, err := svc.Publish(ctx, newer.ID); err != nil {
t.Fatal(err)
}
got, err := svc.Pending(ctx, reader)
if err != nil {
t.Fatal(err)
}
if got == nil || got.ID != newer.ID {
t.Errorf("expected the newest announcement, got %+v", got)
}
}