167 lines
4.8 KiB
Go
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)
|
|
}
|
|
}
|