Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,7 @@ func NewApiServer(config config.Config) *ApiServer {
g.Get("/fan-club/feed", app.v1FanClubFeed)

g.Get("/events/:eventId/comments", app.v1EventComments)
g.Get("/events/:eventId/followers", app.v1EventsFollowers)
g.Get("/events/:eventId/follow_state", app.v1EventFollowState)
g.Get("/events/:eventId/follow-state", app.v1EventFollowState)
g.Post("/events/:eventId/follow", app.requireAuthMiddleware, app.requireWriteScope, app.postV1EventFollow)
Expand Down
34 changes: 34 additions & 0 deletions api/v1_events_followers.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,40 @@ func (app *ApiServer) deleteV1EventFollow(c *fiber.Ctx) error {
})
}

// v1EventsFollowers returns the list of users subscribed to a given
// remix-contest event, ordered by each user's own follower count so the
// most-followed fans surface first in the avatar stack / leaderboard. The
// response shape matches /v1/users/:userId/followers so the same User
// renderers can consume it.
func (app *ApiServer) v1EventsFollowers(c *fiber.Ctx) error {
eventID, err := trashid.DecodeHashId(c.Params("eventId"))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid event id")
}

// `subscriptions.user_id` mirrors the event id (legacy column), so the
// `USING (user_id)` shortcut from v1UsersFollowers would be ambiguous
// here — qualify both joins explicitly against the user being looked
// up (subscriber_id).
sql := `
SELECT subscriptions.subscriber_id
FROM subscriptions
JOIN users ON users.user_id = subscriptions.subscriber_id
JOIN aggregate_user ON aggregate_user.user_id = subscriptions.subscriber_id
WHERE subscriptions.entity_type = 'Event'
AND subscriptions.entity_id = @eventId
AND subscriptions.is_current = true
AND subscriptions.is_delete = false
AND users.is_deactivated = false
ORDER BY aggregate_user.follower_count DESC
LIMIT @limit
OFFSET @offset
`
return app.queryUsers(c, sql, pgx.NamedArgs{
"eventId": eventID,
})
}

// v1EventFollowState returns { is_followed, follower_count } for an event.
// Used by the contest page to render the follow button in the right state.
func (app *ApiServer) v1EventFollowState(c *fiber.Ctx) error {
Expand Down
109 changes: 109 additions & 0 deletions api/v1_events_followers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,3 +491,112 @@ func TestPostEventFollow_DeletedEventIs404(t *testing.T) {
)
assert.Equal(t, 404, status)
}

func TestEventsFollowers_ReturnsOnlyLiveEventSubscribers(t *testing.T) {
// /v1/events/:eventId/followers returns the list of users subscribed
// to the event. Mirrors the shape + filtering rules of follow_state:
// entity_type='Event', is_current & !is_delete, and ignores legacy
// user-type subscriptions with a colliding numeric id.
app := emptyTestApp(t)

database.Seed(app.pool.Replicas[0], database.FixtureMap{
"users": testEventFollowersBaseUsers(),
"tracks": {
{
"track_id": 1,
"owner_id": 1,
"title": "Original",
"created_at": "2020-01-01 00:00:00",
},
},
"events": {
{
"event_id": 200,
"event_type": "remix_contest",
"user_id": 1,
"entity_type": "track",
"entity_id": 1,
"event_data": map[string]any{"description": "remix"},
"created_at": "2020-05-01 00:00:00",
"updated_at": "2020-05-01 00:00:00",
},
},
"subscriptions": []map[string]any{
{
"subscriber_id": 2,
"user_id": 200,
"entity_type": "Event",
"entity_id": 200,
"is_current": true,
"is_delete": false,
"blockhash": "bh1",
"blocknumber": 101,
"txhash": "tx1",
},
{
"subscriber_id": 3,
"user_id": 200,
"entity_type": "Event",
"entity_id": 200,
"is_current": true,
"is_delete": false,
"blockhash": "bh2",
"blocknumber": 101,
"txhash": "tx2",
},
// Legacy user-type subscription with a colliding numeric id —
// must NOT show up.
{
"subscriber_id": 1,
"user_id": 200,
"entity_type": "User",
"entity_id": nil,
"is_current": true,
"is_delete": false,
"blockhash": "bh3",
"blocknumber": 101,
"txhash": "tx3",
},
// Deleted event subscription — must NOT show up.
{
"subscriber_id": 1,
"user_id": 200,
"entity_type": "Event",
"entity_id": 200,
"is_current": true,
"is_delete": true,
"blockhash": "bh4",
"blocknumber": 101,
"txhash": "tx4",
},
},
})

encEvent, err := trashid.EncodeHashId(200)
require.NoError(t, err)
status, body := testGet(t, app, "/v1/events/"+encEvent+"/followers")
require.Equal(t, 200, status, string(body))

jsonAssert(t, body, map[string]any{
"data.#": 2,
})
}

func TestEventsFollowers_UnknownEventReturnsEmptyList(t *testing.T) {
app := emptyTestApp(t)

encEvent, err := trashid.EncodeHashId(9999)
require.NoError(t, err)
status, body := testGet(t, app, "/v1/events/"+encEvent+"/followers")
require.Equal(t, 200, status, string(body))
jsonAssert(t, body, map[string]any{
"data.#": 0,
})
}

func TestEventsFollowers_InvalidEventIdReturns400(t *testing.T) {
app := emptyTestApp(t)

status, _ := testGet(t, app, "/v1/events/not-a-hashid/followers")
assert.Equal(t, 400, status)
}
Loading