From 63a244fe1a5d3220702a048afa976addde4f4645 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 13 Apr 2026 19:28:28 +0200 Subject: [PATCH] Add `/api/v1_alpha/accounts/:id/in_collections` to list collections you are in (#38657) --- .../api/v1_alpha/in_collections_controller.rb | 63 +++++++++++++++++ app/models/concerns/account/associations.rb | 1 + app/policies/account_policy.rb | 4 ++ config/routes/api.rb | 1 + .../api/v1_alpha/in_collections_spec.rb | 69 +++++++++++++++++++ 5 files changed, 138 insertions(+) create mode 100644 app/controllers/api/v1_alpha/in_collections_controller.rb create mode 100644 spec/requests/api/v1_alpha/in_collections_spec.rb diff --git a/app/controllers/api/v1_alpha/in_collections_controller.rb b/app/controllers/api/v1_alpha/in_collections_controller.rb new file mode 100644 index 00000000000..087464989ef --- /dev/null +++ b/app/controllers/api/v1_alpha/in_collections_controller.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +class Api::V1Alpha::InCollectionsController < Api::BaseController + include Authorization + + DEFAULT_COLLECTIONS_LIMIT = 40 + + before_action :check_feature_enabled + + before_action -> { authorize_if_got_token! :read, :'read:collections' }, only: [:index] + + before_action :require_user! + before_action :set_account, only: [:index] + before_action :set_collections, only: [:index] + + after_action :insert_pagination_headers, only: [:index] + + after_action :verify_authorized + + def index + cache_if_unauthenticated! + authorize @account, :index_featured_in_collections? + + render json: @collections, each_serializer: REST::CollectionSerializer, adapter: :json + end + + private + + def set_account + @account = Account.find(params[:account_id]) + end + + def set_collections + @collections = @account.featured_in_collections + .with_tag + .offset(offset_param) + .limit(limit_param(DEFAULT_COLLECTIONS_LIMIT)) + end + + def check_feature_enabled + raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled? + end + + def next_path + return unless records_continue? + + api_v1_alpha_account_in_collections_url(@account, pagination_params(offset: offset_param + limit_param(DEFAULT_COLLECTIONS_LIMIT))) + end + + def prev_path + return if offset_param.zero? + + api_v1_alpha_account_in_collections_url(@account, pagination_params(offset: offset_param - limit_param(DEFAULT_COLLECTIONS_LIMIT))) + end + + def records_continue? + ((offset_param * limit_param(DEFAULT_COLLECTIONS_LIMIT)) + @collections.size) < @account.featured_in_collections.size + end + + def offset_param + params[:offset].to_i + end +end diff --git a/app/models/concerns/account/associations.rb b/app/models/concerns/account/associations.rb index db2e996d0fb..8c26a4da648 100644 --- a/app/models/concerns/account/associations.rb +++ b/app/models/concerns/account/associations.rb @@ -18,6 +18,7 @@ module Account::Associations has_many :collections has_many :collection_items has_many :curated_collection_items, through: :collections, class_name: 'CollectionItem', source: :collection_items + has_many :featured_in_collections, through: :collection_items, class_name: 'Collection', source: :collection has_many :conversations, class_name: 'AccountConversation' has_many :custom_filters has_many :favourites diff --git a/app/policies/account_policy.rb b/app/policies/account_policy.rb index c46eb080348..43c1b8c2a38 100644 --- a/app/policies/account_policy.rb +++ b/app/policies/account_policy.rb @@ -72,4 +72,8 @@ class AccountPolicy < ApplicationPolicy def index_collections? current_account.nil? || !record.blocking_or_domain_blocking?(current_account) end + + def index_featured_in_collections? + current_account.id == record.id + end end diff --git a/config/routes/api.rb b/config/routes/api.rb index e1419bebcb5..2d2cc80fc8e 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -8,6 +8,7 @@ namespace :api, format: false do namespace :v1_alpha do resources :accounts, only: [] do resources :collections, only: [:index] + resources :in_collections, only: [:index] end resources :async_refreshes, only: :show diff --git a/spec/requests/api/v1_alpha/in_collections_spec.rb b/spec/requests/api/v1_alpha/in_collections_spec.rb new file mode 100644 index 00000000000..a4bd3110bed --- /dev/null +++ b/spec/requests/api/v1_alpha/in_collections_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Api::V1Alpha::InCollections', feature: :collections do + include_context 'with API authentication', oauth_scopes: 'read:collections write:collections' + + describe 'GET /api/v1_alpha/in_collections' do + subject do + get "/api/v1_alpha/accounts/#{account.id}/in_collections", headers: headers, params: params + end + + let(:params) { {} } + let(:account) { user.account } + + before { Fabricate.times(3, :collection_item, account: account) } + + it 'returns all collections for the given account and http success' do + subject + + expect(response).to have_http_status(200) + expect(response.parsed_body[:collections].size).to eq 3 + end + + context 'with limit param' do + let(:params) { { limit: '1' } } + + it 'returns only a single result' do + subject + + expect(response).to have_http_status(200) + expect(response.parsed_body[:collections].size).to eq 1 + + expect(response) + .to include_pagination_headers( + next: api_v1_alpha_account_in_collections_url(account, limit: 1, offset: 1) + ) + end + end + + context 'with limit and offset params' do + let(:params) { { limit: '1', offset: '1' } } + + it 'returns the correct result and headers' do + subject + + expect(response).to have_http_status(200) + expect(response.parsed_body[:collections].size).to eq 1 + + expect(response) + .to include_pagination_headers( + prev: api_v1_alpha_account_in_collections_url(account, limit: 1, offset: 0), + next: api_v1_alpha_account_in_collections_url(account, limit: 1, offset: 2) + ) + end + end + + context 'when requested account is different from current account' do + let(:account) { Fabricate(:account) } + + it 'returns http forbidden' do + subject + + expect(response) + .to have_http_status(403) + end + end + end +end