From a9b6236c64bccab06675eb749cb0bd85afbaab1f Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:53:17 -0700 Subject: [PATCH 1/2] Add redirect uri list to dev app response --- api/dbv1/get_developer_apps.sql.go | 37 ++++++++++++++----------- api/dbv1/queries/get_developer_apps.sql | 25 +++++++++-------- api/v1_oauth.go | 1 - 3 files changed, 35 insertions(+), 28 deletions(-) diff --git a/api/dbv1/get_developer_apps.sql.go b/api/dbv1/get_developer_apps.sql.go index 05d886c7..5facc026 100644 --- a/api/dbv1/get_developer_apps.sql.go +++ b/api/dbv1/get_developer_apps.sql.go @@ -15,17 +15,20 @@ import ( const getDeveloperApps = `-- name: GetDeveloperApps :many SELECT - address, - user_id, - name, - description, - image_url -FROM developer_apps -WHERE - (user_id = $1 OR address = $2) - AND is_current = true - AND is_delete = false -ORDER BY created_at DESC + da.address, + da.user_id, + da.name, + da.description, + da.image_url, + COALESCE(ARRAY_AGG(oau.redirect_uri ORDER BY oau.id) FILTER (WHERE oau.redirect_uri IS NOT NULL), ARRAY[]::text[])::text[] AS redirect_uris +FROM developer_apps da +LEFT JOIN oauth_redirect_uris oau ON oau.client_id = da.address +WHERE + (da.user_id = $1 OR da.address = $2) + AND da.is_current = true + AND da.is_delete = false +GROUP BY da.address, da.user_id, da.name, da.description, da.image_url, da.created_at +ORDER BY da.created_at DESC ` type GetDeveloperAppsParams struct { @@ -34,11 +37,12 @@ type GetDeveloperAppsParams struct { } type GetDeveloperAppsRow struct { - Address string `json:"address"` - UserID trashid.HashId `json:"user_id"` - Name string `json:"name"` - Description pgtype.Text `json:"description"` - ImageUrl pgtype.Text `json:"image_url"` + Address string `json:"address"` + UserID trashid.HashId `json:"user_id"` + Name string `json:"name"` + Description pgtype.Text `json:"description"` + ImageUrl pgtype.Text `json:"image_url"` + RedirectUris []string `json:"redirect_uris"` } func (q *Queries) GetDeveloperApps(ctx context.Context, arg GetDeveloperAppsParams) ([]GetDeveloperAppsRow, error) { @@ -56,6 +60,7 @@ func (q *Queries) GetDeveloperApps(ctx context.Context, arg GetDeveloperAppsPara &i.Name, &i.Description, &i.ImageUrl, + &i.RedirectUris, ); err != nil { return nil, err } diff --git a/api/dbv1/queries/get_developer_apps.sql b/api/dbv1/queries/get_developer_apps.sql index 3f769377..4d17c36f 100644 --- a/api/dbv1/queries/get_developer_apps.sql +++ b/api/dbv1/queries/get_developer_apps.sql @@ -1,16 +1,19 @@ -- name: GetDeveloperApps :many SELECT - address, - user_id, - name, - description, - image_url -FROM developer_apps -WHERE - (user_id = @user_id OR address = @address) - AND is_current = true - AND is_delete = false -ORDER BY created_at DESC; + da.address, + da.user_id, + da.name, + da.description, + da.image_url, + COALESCE(ARRAY_AGG(oau.redirect_uri ORDER BY oau.id) FILTER (WHERE oau.redirect_uri IS NOT NULL), ARRAY[]::text[])::text[] AS redirect_uris +FROM developer_apps da +LEFT JOIN oauth_redirect_uris oau ON oau.client_id = da.address +WHERE + (da.user_id = @user_id OR da.address = @address) + AND da.is_current = true + AND da.is_delete = false +GROUP BY da.address, da.user_id, da.name, da.description, da.image_url, da.created_at +ORDER BY da.created_at DESC; -- name: GetDeveloperAppsWithGrants :many SELECT diff --git a/api/v1_oauth.go b/api/v1_oauth.go index 334be08e..6eea837e 100644 --- a/api/v1_oauth.go +++ b/api/v1_oauth.go @@ -538,7 +538,6 @@ func (app *ApiServer) v1OAuthRevoke(c *fiber.Ctx) error { return c.JSON(fiber.Map{}) } - // --- Helper methods --- // validateJWTIat extracts and validates the iat (issued-at) claim from a JWT. From e70017c5fe750ac0313316e93ceb67d0cb4e2e16 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 21:41:34 +0000 Subject: [PATCH 2/2] Add test coverage for redirect_uris in developer app responses Agent-Logs-Url: https://github.com/AudiusProject/api/sessions/a6bf18a2-f093-43e3-b3bd-fe551aef12f8 Co-authored-by: rickyrombo <3690498+rickyrombo@users.noreply.github.com> --- api/v1_developer_apps_test.go | 4 ++ api/v1_users_developer_apps_test.go | 60 +++++++++++++++++++++++++++++ go.mod | 2 +- 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/api/v1_developer_apps_test.go b/api/v1_developer_apps_test.go index 37cb94d9..53cd622e 100644 --- a/api/v1_developer_apps_test.go +++ b/api/v1_developer_apps_test.go @@ -16,6 +16,8 @@ func TestGetDeveloperAppsQueries(t *testing.T) { assert.NoError(t, err) assert.Len(t, developerApps, 1) assert.Equal(t, "0x7d7b6b7a97d1deefe3a1ccc5a13c48e8f055e0b6", developerApps[0].Address) + // redirect_uris must be an empty slice (not nil) when no URIs are registered + assert.Equal(t, []string{}, developerApps[0].RedirectUris) } func TestGetDeveloperApp(t *testing.T) { @@ -29,4 +31,6 @@ func TestGetDeveloperApp(t *testing.T) { assert.True(t, strings.Contains(string(body), `"user_id":"7eP5n"`)) assert.True(t, strings.Contains(string(body), `"name":"cool app"`)) assert.Equal(t, "cool app", resp.Data.Name) + // redirect_uris must be an empty slice (not nil) when no URIs are registered + assert.Equal(t, []string{}, resp.Data.RedirectUris) } diff --git a/api/v1_users_developer_apps_test.go b/api/v1_users_developer_apps_test.go index fe74dda3..c04fe869 100644 --- a/api/v1_users_developer_apps_test.go +++ b/api/v1_users_developer_apps_test.go @@ -7,6 +7,7 @@ import ( "api.audius.co/database" "api.audius.co/trashid" "github.com/stretchr/testify/assert" + "github.com/tidwall/gjson" ) func TestV1UsersDeveloperApps(t *testing.T) { @@ -68,6 +69,11 @@ func TestV1UsersDeveloperApps(t *testing.T) { "data.1.name": "app_name_2", "data.1.description": "app_description_2", }) + // redirect_uris must be an empty array (not null) when no URIs are registered + assert.True(t, gjson.GetBytes(body, "data.0.redirect_uris").IsArray()) + assert.Equal(t, 0, len(gjson.GetBytes(body, "data.0.redirect_uris").Array())) + assert.True(t, gjson.GetBytes(body, "data.1.redirect_uris").IsArray()) + assert.Equal(t, 0, len(gjson.GetBytes(body, "data.1.redirect_uris").Array())) } { @@ -125,3 +131,57 @@ func TestV1UsersDeveloperAppsIncludeMetrics(t *testing.T) { "data.0.api_access_keys.#": 0, }) } + +// TestV1UsersDeveloperAppsRedirectUrisOrdering verifies that redirect_uris are returned +// in deterministic order (by oauth_redirect_uris.id) rather than alphabetically, +// and that apps without registered URIs return an empty array rather than null. +func TestV1UsersDeveloperAppsRedirectUrisOrdering(t *testing.T) { + app := emptyTestApp(t) + + fixtures := database.FixtureMap{ + "users": []map[string]any{ + {"user_id": 1, "handle": "user1"}, + }, + "developer_apps": []map[string]any{ + { + "address": "app_address_1", + "user_id": 1, + "name": "app_name_1", + "is_current": true, + "is_delete": false, + "created_at": time.Now(), + }, + { + "address": "app_address_2", + "user_id": 1, + "name": "app_name_2", + "is_current": true, + "is_delete": false, + "created_at": time.Now().Add(-time.Second), + }, + }, + // Insert z before a: if ordering were alphabetical, a would come first. + // With id ordering, z comes first because it was inserted with a lower id. + "oauth_redirect_uris": []map[string]any{ + {"client_id": "app_address_1", "redirect_uri": "https://z.example.com/callback"}, + {"client_id": "app_address_1", "redirect_uri": "https://a.example.com/callback"}, + }, + } + + database.Seed(app.pool.Replicas[0], fixtures) + + status, body := testGet(t, app, "/v1/users/"+trashid.MustEncodeHashID(1)+"/developer-apps") + assert.Equal(t, 200, status) + // app_address_1 has 2 redirect URIs; z was inserted first (lower id) so it must come first + jsonAssert(t, body, map[string]any{ + "data.#": 2, + "data.0.address": "app_address_1", + "data.0.redirect_uris.#": 2, + "data.0.redirect_uris.0": "https://z.example.com/callback", + "data.0.redirect_uris.1": "https://a.example.com/callback", + "data.1.address": "app_address_2", + }) + // app_address_2 has no redirect URIs; must be an empty array, not null + assert.True(t, gjson.GetBytes(body, "data.1.redirect_uris").IsArray()) + assert.Equal(t, 0, len(gjson.GetBytes(body, "data.1.redirect_uris").Array())) +} diff --git a/go.mod b/go.mod index be4624f7..db7dadb4 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/gofiber/contrib/fiberzap/v2 v2.1.5 github.com/gofiber/contrib/websocket v1.3.4 github.com/gofiber/fiber/v2 v2.52.9 + github.com/google/uuid v1.6.0 github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 github.com/jackc/pgx/v5 v5.7.4 github.com/jackc/pgxlisten v0.0.0-20241106001234-1d6f6656415c @@ -122,7 +123,6 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/orderedcode v0.0.1 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/gorilla/rpc v1.2.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/gowebpki/jcs v1.0.0 // indirect