diff --git a/app/controllers/api/v1_alpha/collections_controller.rb b/app/controllers/api/v1_alpha/collections_controller.rb index 5583bb395d..7c33d3bfa2 100644 --- a/app/controllers/api/v1_alpha/collections_controller.rb +++ b/app/controllers/api/v1_alpha/collections_controller.rb @@ -9,7 +9,14 @@ class Api::V1Alpha::CollectionsController < Api::BaseController before_action -> { doorkeeper_authorize! :write, :'write:collections' }, only: [:create] - before_action :require_user! + before_action :require_user!, only: [:create] + + def show + cache_if_unauthenticated! + @collection = Collection.find(params[:id]) + + render json: @collection, serializer: REST::CollectionSerializer + end def create @collection = CreateCollectionService.new.call(collection_params, current_user.account) diff --git a/app/models/collection.rb b/app/models/collection.rb index 41f9ed0f02..308e517e26 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -38,10 +38,18 @@ class Collection < ApplicationRecord validate :tag_is_usable validate :items_do_not_exceed_limit + scope :with_items, -> { includes(:collection_items).merge(CollectionItem.with_accounts) } + def remote? !local? end + def items_for(account = nil) + result = collection_items.with_accounts + result = result.not_blocked_by(account) unless account.nil? + result + end + private def tag_is_usable diff --git a/app/models/collection_item.rb b/app/models/collection_item.rb index 0ea50e6914..48a18592fd 100644 --- a/app/models/collection_item.rb +++ b/app/models/collection_item.rb @@ -33,6 +33,8 @@ class CollectionItem < ApplicationRecord validates :object_uri, presence: true, if: -> { account.nil? } scope :ordered, -> { order(position: :asc) } + scope :with_accounts, -> { includes(account: [:account_stat, :user]) } + scope :not_blocked_by, ->(account) { where.not(accounts: { id: account.blocking }) } def local_item_with_remote_account? local? && account&.remote? diff --git a/app/serializers/rest/collection_item_serializer.rb b/app/serializers/rest/collection_item_serializer.rb new file mode 100644 index 0000000000..c0acc87bfd --- /dev/null +++ b/app/serializers/rest/collection_item_serializer.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class REST::CollectionItemSerializer < ActiveModel::Serializer + delegate :accepted?, to: :object + + attributes :position, :state + + belongs_to :account, serializer: REST::AccountSerializer, if: :accepted? +end diff --git a/app/serializers/rest/collection_serializer.rb b/app/serializers/rest/collection_serializer.rb index c03cc53856..3d63e75398 100644 --- a/app/serializers/rest/collection_serializer.rb +++ b/app/serializers/rest/collection_serializer.rb @@ -5,4 +5,11 @@ class REST::CollectionSerializer < ActiveModel::Serializer :created_at, :updated_at belongs_to :account, serializer: REST::AccountSerializer + belongs_to :tag, serializer: REST::StatusSerializer::TagSerializer + + has_many :items, serializer: REST::CollectionItemSerializer + + def items + object.items_for(current_user&.account) + end end diff --git a/config/routes/api.rb b/config/routes/api.rb index 2fa3d4d833..32685d791f 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -8,7 +8,7 @@ namespace :api, format: false do namespace :v1_alpha do resources :async_refreshes, only: :show - resources :collections, only: [:create] + resources :collections, only: [:show, :create] end # JSON / REST API diff --git a/spec/fabricators/collection_item_fabricator.rb b/spec/fabricators/collection_item_fabricator.rb index 011f9ba5b5..db55f7d237 100644 --- a/spec/fabricators/collection_item_fabricator.rb +++ b/spec/fabricators/collection_item_fabricator.rb @@ -3,7 +3,7 @@ Fabricator(:collection_item) do collection { Fabricate.build(:collection) } account { Fabricate.build(:account) } - position 1 + position { sequence(:position, 1) } state :accepted end diff --git a/spec/models/collection_spec.rb b/spec/models/collection_spec.rb index 6c160ecb70..3d367b9d90 100644 --- a/spec/models/collection_spec.rb +++ b/spec/models/collection_spec.rb @@ -48,4 +48,28 @@ RSpec.describe Collection do it { is_expected.to_not be_valid } end end + + describe '#item_for' do + subject { Fabricate(:collection) } + + let!(:items) { Fabricate.times(2, :collection_item, collection: subject) } + + context 'when given no account' do + it 'returns all items' do + expect(subject.items_for).to match_array(items) + end + end + + context 'when given an account' do + let(:account) { Fabricate(:account) } + + before do + account.block!(items.first.account) + end + + it 'does not return items blocked by this account' do + expect(subject.items_for(account)).to contain_exactly(items.last) + end + end + end end diff --git a/spec/requests/api/v1_alpha/collections_spec.rb b/spec/requests/api/v1_alpha/collections_spec.rb index 5f9c5e5f34..d576fbf98b 100644 --- a/spec/requests/api/v1_alpha/collections_spec.rb +++ b/spec/requests/api/v1_alpha/collections_spec.rb @@ -5,6 +5,50 @@ require 'rails_helper' RSpec.describe 'Api::V1Alpha::Collections', feature: :collections do include_context 'with API authentication', oauth_scopes: 'read:collections write:collections' + describe 'GET /api/v1_alpha/collections/:id' do + subject do + get "/api/v1_alpha/collections/#{collection.id}", headers: headers + end + + let(:collection) { Fabricate(:collection) } + let!(:items) { Fabricate.times(2, :collection_item, collection:) } + + shared_examples 'unfiltered, successful request' do + it 'includes all items in the response' do + subject + + expect(response).to have_http_status(200) + expect(response.parsed_body[:items].size).to eq 2 + end + end + + context 'when user is not signed in' do + let(:headers) { {} } + + it_behaves_like 'unfiltered, successful request' + end + + context 'when user is signed in' do + context 'when the user has not blocked or muted anyone' do + it_behaves_like 'unfiltered, successful request' + end + + context 'when the user has blocked an account' do + before do + user.account.block!(items.first.account) + end + + it 'only includes the non-blocked account in the response' do + subject + + expect(response).to have_http_status(200) + expect(response.parsed_body[:items].size).to eq 1 + expect(response.parsed_body[:items][0]['position']).to eq items.last.position + end + end + end + end + describe 'POST /api/v1_alpha/collections' do subject do post '/api/v1_alpha/collections', headers: headers, params: params diff --git a/spec/serializers/rest/collection_item_serializer_spec.rb b/spec/serializers/rest/collection_item_serializer_spec.rb new file mode 100644 index 0000000000..bcb7458c4d --- /dev/null +++ b/spec/serializers/rest/collection_item_serializer_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe REST::CollectionItemSerializer do + subject { serialized_record_json(collection_item, described_class) } + + let(:collection_item) do + Fabricate(:collection_item, + state:, + position: 4) + end + + context 'when state is `accepted`' do + let(:state) { :accepted } + + it 'includes the relevant attributes including the account' do + expect(subject) + .to include( + 'account' => an_instance_of(Hash), + 'state' => 'accepted', + 'position' => 4 + ) + end + end + + %i(pending rejected revoked).each do |unaccepted_state| + context "when state is `#{unaccepted_state}`" do + let(:state) { unaccepted_state } + + it 'does not include an account' do + expect(subject.keys).to_not include('account') + end + end + end +end diff --git a/spec/serializers/rest/collection_serializer_spec.rb b/spec/serializers/rest/collection_serializer_spec.rb index c498937b50..10bf9ee2b5 100644 --- a/spec/serializers/rest/collection_serializer_spec.rb +++ b/spec/serializers/rest/collection_serializer_spec.rb @@ -3,15 +3,24 @@ require 'rails_helper' RSpec.describe REST::CollectionSerializer do - subject { serialized_record_json(collection, described_class) } + subject do + serialized_record_json(collection, described_class, options: { + scope: current_user, + scope_name: :current_user, + }) + end + let(:current_user) { nil } + + let(:tag) { Fabricate(:tag, name: 'discovery') } let(:collection) do Fabricate(:collection, name: 'Exquisite follows', description: 'Always worth a follow', local: true, sensitive: true, - discoverable: false) + discoverable: false, + tag:) end it 'includes the relevant attributes' do @@ -23,6 +32,7 @@ RSpec.describe REST::CollectionSerializer do 'local' => true, 'sensitive' => true, 'discoverable' => false, + 'tag' => a_hash_including('name' => 'discovery'), 'created_at' => match_api_datetime_format, 'updated_at' => match_api_datetime_format )