Skip to content
Closed
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
15 changes: 9 additions & 6 deletions api/dbv1/get_tracks.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion api/dbv1/queries/get_tracks.sql
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,10 @@ FROM tracks t
JOIN aggregate_track using (track_id)
LEFT JOIN aggregate_plays on play_item_id = t.track_id
LEFT JOIN track_routes on t.track_id = track_routes.track_id and track_routes.is_current = true
WHERE (is_unlisted = false OR t.owner_id = @my_id OR @include_unlisted::bool = TRUE)
WHERE (is_unlisted = false OR t.owner_id = @my_id OR @include_unlisted::bool = TRUE
OR (COALESCE(@authed_wallet, '') <> ''
AND t.access_authorities IS NOT NULL
AND EXISTS (SELECT 1 FROM unnest(t.access_authorities) aa WHERE lower(aa) = lower(@authed_wallet))))
Comment on lines +223 to +224
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The access-authority wallet match (EXISTS (SELECT 1 FROM unnest(t.access_authorities) ...)) is computed twice now: once inside the is_unlisted OR clause and again in the existing AND (t.access_authorities IS NULL OR ...) gate below. Since the AND clause already enforces the wallet match whenever access_authorities is non-NULL, the OR clause only needs to check that @authed_wallet is set and t.access_authorities is non-NULL; duplicating the EXISTS/unnest adds per-row work and makes the predicate harder to maintain.

Suggested change
AND t.access_authorities IS NOT NULL
AND EXISTS (SELECT 1 FROM unnest(t.access_authorities) aa WHERE lower(aa) = lower(@authed_wallet))))
AND t.access_authorities IS NOT NULL))

Copilot uses AI. Check for mistakes.
AND t.track_id = ANY(@ids::int[])
AND (t.access_authorities IS NULL
OR (COALESCE(@authed_wallet, '') <> ''
Expand Down
26 changes: 26 additions & 0 deletions api/v1_tracks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,29 @@ func TestGetTracksExcludesAccessAuthorities(t *testing.T) {
assert.Equal(t, "T1", resp.Data[0].Title.String)
assert.Equal(t, []string{gateWallet}, resp.Data[0].AccessAuthorities)
}

func TestGetUnlistedTrackWithAccessAuthority(t *testing.T) {
app := testAppWithFixtures(t)
ctx := context.Background()
require.NotNil(t, app.writePool, "test requires write pool")

gateWallet := "0x7d273271690538cf855e5b3002a0dd8c154bb060"
// Make track 100 both unlisted and gated by access_authorities
_, err := app.writePool.Exec(ctx, `UPDATE tracks SET is_unlisted = true, access_authorities = ARRAY[$1]::text[] WHERE track_id = 100 AND is_current = true`, gateWallet)
require.NoError(t, err)

var resp struct {
Data []dbv1.Track
}

// Without auth: unlisted + gated track must not be returned
status, _ := testGet(t, app, "/v1/full/tracks?id=eYZmn", &resp)
assert.Equal(t, 200, status)
assert.Len(t, resp.Data, 0, "unlisted track with access_authorities must not be returned when unauthenticated")

// With auth signed by access authority: unlisted track must be returned
status, _ = testGetWithWallet(t, app, "/v1/full/tracks?id=eYZmn", gateWallet, &resp)
Comment on lines +80 to +86
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test hits /v1/full/tracks, which (per handler implementation) always sets IncludeUnlisted: true, so the unlisted filter is already bypassed. As a result, this test would still pass even without the new is_unlisted/authed_wallet SQL change, and doesn’t actually validate the new behavior (unlisted track visibility when IncludeUnlisted is false). Consider exercising the db query directly (e.g., app.queries.Tracks / GetTracks with IncludeUnlisted: false) or using an endpoint/path that does not force-include unlisted tracks.

Suggested change
// Without auth: unlisted + gated track must not be returned
status, _ := testGet(t, app, "/v1/full/tracks?id=eYZmn", &resp)
assert.Equal(t, 200, status)
assert.Len(t, resp.Data, 0, "unlisted track with access_authorities must not be returned when unauthenticated")
// With auth signed by access authority: unlisted track must be returned
status, _ = testGetWithWallet(t, app, "/v1/full/tracks?id=eYZmn", gateWallet, &resp)
// Use the non-full tracks endpoint so IncludeUnlisted is not force-enabled.
// Without auth: unlisted + gated track must not be returned.
status, _ := testGet(t, app, "/v1/tracks?id=eYZmn", &resp)
assert.Equal(t, 200, status)
assert.Len(t, resp.Data, 0, "unlisted track with access_authorities must not be returned when unauthenticated")
// With auth signed by access authority: unlisted track must be returned.
status, _ = testGetWithWallet(t, app, "/v1/tracks?id=eYZmn", gateWallet, &resp)

Copilot uses AI. Check for mistakes.
assert.Equal(t, 200, status)
assert.Len(t, resp.Data, 1, "unlisted track with access_authorities must be returned when request is signed by matching authority")
assert.Equal(t, "T1", resp.Data[0].Title.String)
}
Loading