//go:build integration package social import ( "context" "github.com/google/uuid" "github.com/stretchr/testify/require" "os" "pipu/api/internal/api/gen" "pipu/api/internal/platform/db" "sync" "testing" ) func TestSocialIsolationAndLifecycle(t *testing.T) { url := os.Getenv("TEST_DATABASE_URL") if url == "" { t.Skip("TEST_DATABASE_URL is not set") } ctx := context.Background() pool, e := db.Connect(ctx, url) require.NoError(t, e) defer pool.Close() require.NoError(t, db.Migrate(ctx, pool)) s := &Store{pool: pool} a, b, c := uuid.New(), uuid.New(), uuid.New() for _, id := range []uuid.UUID{a, b, c} { _, e = pool.Exec(ctx, `INSERT INTO users(id,username,password_hash) VALUES($1,$2,'x')`, id, id.String()) require.NoError(t, e) } defer pool.Exec(ctx, `DELETE FROM users WHERE id=ANY($1)`, []uuid.UUID{a, b, c}) action := func(user, peer uuid.UUID, op string) error { return s.action(ctx, user, gen.SocialAction{Action: gen.SocialActionAction(op), TargetId: peer}) } require.NoError(t, action(a, b, "request")) require.NoError(t, action(a, b, "request")) parallel(t, func() error { return action(a, b, "request") }) inbox, e := s.inbox(ctx, b) require.NoError(t, e) require.Len(t, inbox.Notifications, 1) require.Equal(t, 1, inbox.Unread) require.EqualValues(t, "incoming", inbox.People[0].Relationship) require.Error(t, action(a, b, "accept")) require.Error(t, action(c, b, "accept")) _, e = s.send(ctx, a, b, gen.SendDirectMessage{Id: uuid.New(), Text: "before acceptance", AttachmentKind: "none"}) require.Error(t, e) require.NoError(t, action(b, a, "accept")) require.NoError(t, action(b, a, "accept")) inbox, e = s.inbox(ctx, a) require.NoError(t, e) require.Len(t, inbox.Notifications, 1) require.EqualValues(t, "friend", inbox.People[0].Relationship) body := gen.SendDirectMessage{Id: uuid.New(), Text: "Привет", AttachmentKind: "none"} m, e := s.send(ctx, a, b, body) require.NoError(t, e) again, e := s.send(ctx, a, b, body) require.NoError(t, e) require.Equal(t, m.Seq, again.Seq) parallel(t, func() error { _, err := s.send(ctx, a, b, body); return err }) body.Text = "changed" _, e = s.send(ctx, a, b, body) require.Error(t, e) _, e = s.messages(ctx, c, b, nil) require.Error(t, e) inbox, e = s.inbox(ctx, b) require.NoError(t, e) require.Equal(t, 1, inbox.Unread) // Another user cannot acknowledge this user's notification. require.NoError(t, s.action(ctx, c, gen.SocialAction{Action: "read_notification", TargetId: inbox.Notifications[0].Id})) inbox, e = s.inbox(ctx, b) require.NoError(t, e) require.Equal(t, 1, inbox.Unread) second, e := s.send(ctx, a, b, gen.SendDirectMessage{Id: uuid.New(), Text: "Later", AttachmentKind: "none"}) require.NoError(t, e) require.NoError(t, s.action(ctx, b, gen.SocialAction{Action: "read_chat", TargetId: a, UpToSeq: &m.Seq})) history, e := s.messages(ctx, a, b, nil) require.NoError(t, e) require.Len(t, history, 2) require.True(t, history[0].Read) require.False(t, history[1].Read) before, e := s.messages(ctx, b, a, &second.Seq) require.NoError(t, e) require.Len(t, before, 1) require.NoError(t, action(b, a, "block")) require.NoError(t, action(a, b, "block")) for _, id := range []uuid.UUID{a, b} { blocked, err := s.inbox(ctx, id) require.NoError(t, err) require.Len(t, blocked.People, 1) require.EqualValues(t, "blocked", blocked.People[0].Relationship) } require.NoError(t, action(a, b, "unblock")) _, e = s.send(ctx, a, b, gen.SendDirectMessage{Id: uuid.New(), Text: "blocked", AttachmentKind: "none"}) require.Error(t, e) _, e = s.messages(ctx, b, a, nil) require.Error(t, e) require.Error(t, action(a, b, "request")) found, e := s.people(ctx, a, b.String()) require.NoError(t, e) require.Empty(t, found) require.NoError(t, action(b, a, "unblock")) require.Error(t, action(a, b, "accept")) require.NoError(t, action(a, b, "request")) require.NoError(t, action(b, a, "accept")) require.NoError(t, action(a, b, "remove")) _, e = s.messages(ctx, b, a, nil) require.Error(t, e) // An attachment must reference available catalog music or an active playlist share. require.NoError(t, action(a, c, "request")) require.NoError(t, action(c, a, "accept")) missing := uuid.New() _, e = s.send(ctx, a, c, gen.SendDirectMessage{Id: uuid.New(), AttachmentKind: "playlist", AttachmentId: &missing}) require.Error(t, e) playlist, share := uuid.New(), uuid.New() _, e = pool.Exec(ctx, `INSERT INTO playlists(id,user_id,name,share_id) VALUES($1,$2,'Music',$3)`, playlist, a, share) require.NoError(t, e) attachment, e := s.send(ctx, a, c, gen.SendDirectMessage{Id: uuid.New(), AttachmentKind: "playlist", AttachmentId: &share}) require.NoError(t, e) require.Equal(t, &share, attachment.AttachmentId) _, e = pool.Exec(ctx, `UPDATE playlists SET share_id=NULL WHERE id=$1`, playlist) require.NoError(t, e) _, e = s.send(ctx, a, c, gen.SendDirectMessage{Id: uuid.New(), AttachmentKind: "playlist", AttachmentId: &share}) require.Error(t, e) found, e = s.people(ctx, a, "%_") require.NoError(t, e) require.Empty(t, found) } func parallel(t *testing.T, operation func() error) { t.Helper() var wg sync.WaitGroup errors := make(chan error, 8) for range 8 { wg.Add(1) go func() { defer wg.Done(); errors <- operation() }() } wg.Wait() close(errors) for err := range errors { require.NoError(t, err) } }