# frozen_string_literal: true require_relative "api_spec_helper" describe Api::V1::SearchController do let(:auth) { FactoryBot.create( :auth_with_default_scopes, scopes: %w[openid public:read public:modify private:read contacts:read private:modify] ) } let(:auth_read_only) { FactoryBot.create( :auth_with_default_scopes, scopes: %w[openid public:read private:read] ) } let(:auth_public_only_read_only) { FactoryBot.create( :auth_with_default_scopes, scopes: %w[openid public:read] ) } let!(:access_token) { auth.create_access_token.to_s } let!(:access_token_read_only) { auth_read_only.create_access_token.to_s } let!(:access_token_public_only_read_only) { auth_public_only_read_only.create_access_token.to_s } let(:invalid_token) { SecureRandom.hex(9) } describe "#user_index" do before do @searchable_user = FactoryBot.create( :person, diaspora_handle: "findable@example.org", profile: FactoryBot.build(:profile, first_name: "Terry", last_name: "Smith") ) @closed_user = FactoryBot.create( :person, closed_account: true, profile: FactoryBot.build(:profile, first_name: "Closed", last_name: "Account") ) @unsearchable_user = FactoryBot.create( :person, diaspora_handle: "unsearchable@example.org", profile: FactoryBot.build( :profile, first_name: "Unsearchable", last_name: "Person", searchable: false ) ) end it "succeeds by tag" do tag = SecureRandom.hex(5) 5.times do FactoryBot.create(:person, profile: FactoryBot.build(:profile, tag_string: "##{tag}")) end FactoryBot.create(:person, closed_account: true, profile: FactoryBot.build(:profile, tag_string: "##{tag}")) get( "/api/v1/search/users", params: {tag: tag, access_token: access_token} ) expect(response.status).to eq(200) users = response_body_data(response) expect(users.length).to eq(5) expect_to_match_json_schema(users.to_json, "#/definitions/users") end it "succeeds by name" do get( "/api/v1/search/users", params: {name_or_handle: "Terry", access_token: access_token} ) expect(response.status).to eq(200) users = response_body_data(response) expect(users.length).to eq(1) expect_to_match_json_schema(users.to_json, "#/definitions/users") end it "succeeds by handle" do get( "/api/v1/search/users", params: {name_or_handle: "findable", access_token: access_token} ) expect(response.status).to eq(200) users = response_body_data(response) expect(users.length).to eq(1) expect_to_match_json_schema(users.to_json, "#/definitions/users") end context "with a contacts filter" do let(:name) { "A contact" } it "only returns contacts" do add_contact(true, false) add_contact(false, true) add_contact(true, true) get( "/api/v1/search/users", params: { name_or_handle: name, filter: "contacts", access_token: access_token } ) expect(response.status).to eq(200) users = response_body_data(response) expect(users.length).to eq(3) end it "only returns receiving contacts" do add_contact(true, false) add_contact(true, false) add_contact(false, true) get( "/api/v1/search/users", params: { name_or_handle: name, filter: "contacts:receiving", access_token: access_token } ) expect(response.status).to eq(200) users = response_body_data(response) expect(users.length).to eq(2) end it "only returns sharing contacts" do add_contact(true, false) add_contact(false, true) add_contact(false, true) get( "/api/v1/search/users", params: { name_or_handle: name, filter: "contacts:sharing", access_token: access_token } ) expect(response.status).to eq(200) users = response_body_data(response) expect(users.length).to eq(2) end it "only returns mutually sharing contacts" do add_contact(true, false) add_contact(false, true) add_contact(true, true) get( "/api/v1/search/users", params: { name_or_handle: name, filter: ["contacts:receiving", "contacts:sharing"], access_token: access_token } ) expect(response.status).to eq(200) users = response_body_data(response) expect(users.length).to eq(1) end it "fails with an invalid filter" do get( "/api/v1/search/users", params: { name_or_handle: name, filter: "contacts:thingsiwant", access_token: access_token } ) confirm_api_error(response, 422, "Invalid filter") end it "fails without contacts:read scope" do get( "/api/v1/search/users", params: { name_or_handle: name, filter: "contacts", access_token: access_token_read_only } ) confirm_api_error(response, 403, "insufficient_scope") end def add_contact(receiving, sharing) other = FactoryBot.create(:user) other.profile.update(first_name: name) if receiving aspect = auth.user.aspects.find_or_create_by(name: "Test") auth.user.share_with(other.person, aspect) end if sharing # rubocop:disable Style/GuardClause aspect = other.aspects.create(name: "Test") other.share_with(auth.user.person, aspect) end end end context "with an aspects filter" do let(:contact_name) { "My aspect contact" } let(:aspect) { auth.user.aspects.create(name: "Test") } let(:second_aspect) { auth.user.aspects.create(name: "Second test") } it "only returns members of given aspects" do add_contact(aspect) add_contact(second_aspect) get( "/api/v1/search/users", params: { name_or_handle: contact_name, filter: "aspect:#{aspect.id}", access_token: access_token } ) expect(response.status).to eq(200) users = response_body_data(response) expect(users.length).to eq(1) get( "/api/v1/search/users", params: { name_or_handle: contact_name, filter: "aspect:#{aspect.id},#{second_aspect.id}", access_token: access_token } ) expect(response.status).to eq(200) users = response_body_data(response) expect(users.length).to eq(2) end it "only returns people matching all aspect filters" do add_contact(aspect) add_contact(second_aspect) add_contact(aspect, second_aspect) get( "/api/v1/search/users", params: { name_or_handle: contact_name, filter: ["aspect:#{aspect.id}", "aspect:#{second_aspect.id}"], access_token: access_token } ) expect(response.status).to eq(200) users = response_body_data(response) expect(users.length).to eq(1) end it "fails with an invalid aspect" do get( "/api/v1/search/users", params: { name_or_handle: contact_name, filter: "aspect:0", access_token: access_token } ) confirm_api_error(response, 422, "Invalid aspect filter") get( "/api/v1/search/users", params: { name_or_handle: contact_name, filter: "aspect:#{aspect.id},0", access_token: access_token } ) confirm_api_error(response, 422, "Invalid aspect filter") end it "fails without contacts:read scope" do aspect = auth_read_only.user.aspects.create(name: "Test") get( "/api/v1/search/users", params: { name_or_handle: contact_name, filter: "aspect:#{aspect.id}", access_token: access_token_read_only } ) confirm_api_error(response, 403, "insufficient_scope") end def add_contact(*aspects) other = FactoryBot.create(:person, profile: FactoryBot.build(:profile, first_name: contact_name)) aspects.each do |aspect| auth.user.share_with(other, aspect) end end end it "fails with an invalid filter" do get( "/api/v1/search/users", params: { name_or_handle: "findable", filter: "thingsiwant", access_token: access_token } ) confirm_api_error(response, 422, "Invalid filter") end it "doesn't return closed accounts" do get( "/api/v1/search/users", params: {name_or_handle: "Closed", access_token: access_token} ) expect(response.status).to eq(200) users = response_body_data(response) expect(users.length).to eq(0) end it "doesn't return hidden accounts" do get( "/api/v1/search/users", params: {name_or_handle: "unsearchable@example.org", access_token: access_token} ) expect(response.status).to eq(200) users = response_body_data(response) expect(users.length).to eq(0) end it "doesn't return hidden accounts who are linked without contacts:read token" do aspect_to = auth_public_only_read_only.user.aspects.create(name: "shared aspect") auth_public_only_read_only.user.share_with(@unsearchable_user, aspect_to) get( "/api/v1/search/users", params: {name_or_handle: "unsearchable@example.org", access_token: access_token_public_only_read_only} ) expect(response.status).to eq(200) users = response_body_data(response) expect(users.length).to eq(0) end it "fails if ask for both" do get( "/api/v1/search/users", params: {tag: "tag1", name_or_handle: "name", access_token: access_token} ) confirm_api_error(response, 422, "Parameters tag and name_or_handle are exclusive") end it "fails with no fields" do get( "/api/v1/search/users", params: {access_token: access_token} ) confirm_api_error(response, 422, "Missing parameter tag or name_or_handle") end it "fails with bad credentials" do get( "/api/v1/search/users", params: {tag: "tag1", access_token: invalid_token} ) expect(response.status).to eq(401) end end describe "post_index" do before do @user_post = auth.user.post( :status_message, text: "This is a status message #tag1 #tag2", public: true ) @eve_post = eve.post( :status_message, text: "This is Eve's status message #tag2 #tag3", public: true ) aspect = eve.aspects.create(name: "shared aspect") eve.share_with(auth_public_only_read_only.user.person, aspect) eve.share_with(auth.user.person, aspect) @eve_private_post = eve.post( :status_message, text: "This is Eve's status message #tag2 #tag3", public: false, to: aspect.id ) end it "succeeds by tag" do get( "/api/v1/search/posts", params: {tag: "tag2", access_token: access_token_public_only_read_only} ) expect(response.status).to eq(200) posts = response_body_data(response) expect(posts.length).to eq(2) expect_to_match_json_schema(posts.to_json, "#/definitions/posts") end it "only returns public posts without private scope" do get( "/api/v1/search/posts", params: {tag: "tag2", access_token: access_token_public_only_read_only} ) expect(response.status).to eq(200) posts = response_body_data(response) expect(posts.length).to eq(2) get( "/api/v1/search/posts", params: {tag: "tag2", access_token: access_token} ) expect(response.status).to eq(200) posts = response_body_data(response) expect(posts.length).to eq(3) end it "fails with missing parameters" do get( "/api/v1/search/posts", params: {access_token: access_token} ) confirm_api_error(response, 422, "param is missing or the value is empty: tag") end it "fails with bad credentials" do get( "/api/v1/search/posts", params: {tag: "tag1", access_token: invalid_token} ) expect(response.status).to eq(401) end end describe "tag_index" do before do FactoryBot.create(:tag, name: "apipartyone") FactoryBot.create(:tag, name: "apipartytwo") FactoryBot.create(:tag, name: "apipartythree") end it "succeeds" do get( "/api/v1/search/tags", params: {query: "apiparty", access_token: access_token_public_only_read_only} ) expect(response.status).to eq(200) tags = response_body_data(response) expect(tags.size).to eq(3) expect_to_match_json_schema(tags.to_json, "#/definitions/tags") end it "does a prefix search" do get( "/api/v1/search/tags", params: {query: "apipartyt", access_token: access_token_public_only_read_only} ) expect(response.status).to eq(200) tags = response_body_data(response) expect(tags.size).to eq(2) expect_to_match_json_schema(tags.to_json, "#/definitions/tags") end it "fails with missing parameters" do get( "/api/v1/search/tags", params: {access_token: access_token} ) confirm_api_error(response, 422, "param is missing or the value is empty: query") end it "fails with bad credentials" do get( "/api/v1/search/tags", params: {query: "apiparty", access_token: invalid_token} ) expect(response.status).to eq(401) end end def response_body_data(response) JSON.parse(response.body) end end