Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Collaborators API #1473

Merged
merged 10 commits into from Jun 25, 2020
63 changes: 63 additions & 0 deletions app/controllers/api/v1/collaborators_controller.rb
@@ -0,0 +1,63 @@
# frozen_string_literal: true

class Api::V1::CollaboratorsController < Api::V1::BaseController
before_action :authenticate_user!
before_action :set_project
before_action :check_author_access, except: %i[index]
before_action :check_view_access, only: %i[index]
before_action :set_collaborator, only: %i[destroy]

# /api/v1/projects/:project_id/collaborators
def index
@collaborators = paginate(@project.collaborators)
Nitish145 marked this conversation as resolved.
Show resolved Hide resolved
# options for serializing collaborators
@options = {
params: { only_name: true },
links: link_attrs(@collaborators, api_v1_project_collaborators_url)
}
render json: Api::V1::UserSerializer.new(@collaborators, @options)
end

# POST /api/v1/projects/:project_id/collaborators
def create
mails_handler = MailsHandler.new(params[:emails], @project, @current_user)
# parse mails as valid or invalid
mails_handler.parse

render json: {
added: mails_handler.added_mails,
existing: mails_handler.existing_mails,
invalid: mails_handler.invalid_mails
}
end

# DELETE /api/v1//projects/:project_id/collaborators/:id
# :id is essentially the user_id for the user to be removed from project
def destroy
@collaboration = Collaboration.find_by(user: @collaborator, project: @project)
@collaboration.destroy!
render json: {}, status: :no_content
end

private

def set_project
@project = Project.find(params[:project_id])
end

def check_author_access
authorize @project, :author_access?
end

def check_view_access
authorize @project, :check_view_access?
end

def set_collaborator
@collaborator = @project.collaborators.find(params[:id])
end

def collaborator_params
params.require(:collaborator).permit(:project_id, :emails)
end
end
2 changes: 1 addition & 1 deletion app/controllers/api/v1/users_controller.rb
Expand Up @@ -9,7 +9,7 @@ class Api::V1::UsersController < Api::V1::BaseController
# GET api/v1/users
def index
@users = paginate(User.all)
@options = { params: { are_all_users_fetched: true } }
@options = { params: { only_name: true } }
@options[:links] = link_attrs(@users, api_v1_users_url)
render json: Api::V1::UserSerializer.new(@users, @options)
end
Expand Down
4 changes: 2 additions & 2 deletions app/serializers/api/v1/user_serializer.rb
Expand Up @@ -3,7 +3,7 @@
class Api::V1::UserSerializer
include FastJsonapi::ObjectSerializer

# only name is serialized if all users are fetched
# only name is serialized if all users/collaborators are fetched
attribute :name

# only serialized if user fetches own details
Expand All @@ -14,6 +14,6 @@ class Api::V1::UserSerializer

attributes :admin, :country, :educational_institute,
if: proc { |record, params|
params[:are_all_users_fetched] != true || record.admin
params[:only_name] != true || record.admin
}
end
48 changes: 48 additions & 0 deletions app/services/api/v1/collaborators_controller/mails_handler.rb
@@ -0,0 +1,48 @@
# frozen_string_literal: true

class Api::V1::CollaboratorsController
class MailsHandler
attr_reader :valid_mails, :invalid_mails, :existing_mails

# initialize the class with mails, project and current_user to be used in class
def initialize(mails, project, current_user)
@mails = mails
@project = project
@current_user = current_user
# initialize empty valid and invalid mails
@valid_mails = []
@invalid_mails = []
end

# parse emails as valid, invalid or existing mails
def parse
@mails.split(",").each do |email|
email = email.strip
if email.present? && email != @current_user.email && Devise.email_regexp.match?(email)
@valid_mails.push(email)
else
@invalid_mails.push(email)
end
end

@existing_mails = User.where(
id: @project.collaborations.pluck(:user_id)
).pluck(:email)
end

def added_mails
newly_added = valid_mails - existing_mails
added_mails = []
# checks if user exists and adds to added_mails
newly_added.each do |email|
user = User.find_by(email: email)
if user.present?
added_mails.push(email)
Collaboration.where(project_id: @project.id, user_id: user.id).first_or_create
end
end
# returns added_mails
added_mails
end
end
end
1 change: 1 addition & 0 deletions config/routes.rb
Expand Up @@ -129,6 +129,7 @@
get 'fork', to: 'projects#create_fork'
get 'image_preview', to: 'projects#image_preview'
end
resources :collaborators, only: [:index, :create, :destroy]
end
resources :users do
get 'projects', to: 'projects#user_projects', on: :member
Expand Down
79 changes: 79 additions & 0 deletions spec/requests/api/v1/collaborators_controller/create_spec.rb
@@ -0,0 +1,79 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe Api::V1::CollaboratorsController, "#create", type: :request do
describe "create/add collaborators" do
let!(:author) { FactoryBot.create(:user) }
let!(:project) { FactoryBot.create(:project, author: author) }
let!(:user) { FactoryBot.create(:user) }

context "when not authenticated" do
before do
post "/api/v1/projects/#{project.id}/collaborators/", as: :json
end

it "returns status unauthenticated" do
expect(response).to have_http_status(401)
expect(response.parsed_body).to have_jsonapi_errors
end
end

context "when authenticated as random user and don't have author_access?" do
before do
token = get_auth_token(FactoryBot.create(:user))
post "/api/v1/projects/#{project.id}/collaborators/",
headers: { "Authorization": "Token #{token}" },
params: create_params, as: :json
end

it "returns status unauthorized" do
expect(response).to have_http_status(403)
expect(response.parsed_body).to have_jsonapi_errors
end
end

context "when authorized but tries to add collaborator to non existent project" do
before do
token = get_auth_token(author)
post "/api/v1/projects/0/collaborators/",
headers: { "Authorization": "Token #{token}" },
params: create_params, as: :json
end

it "returns status not_found" do
expect(response).to have_http_status(404)
expect(response.parsed_body).to have_jsonapi_errors
end
end

context "when authorized and has access to add collaborator" do
before do
# creates a collaboration
existing = FactoryBot.create(:user, email: "existing@test.com")
FactoryBot.create(:collaboration, user: existing, project: project)
token = get_auth_token(author)
post "/api/v1/projects/#{project.id}/collaborators/",
headers: { "Authorization": "Token #{token}" },
params: create_params, as: :json
end

it "returns status code 200" do
expect(response).to have_http_status(200)
end

it "returns the added, already_existing & invalid mails (author being invalid)" do
expect(response.parsed_body["added"]).to eq([user.email])
puts user.email
expect(response.parsed_body["existing"]).to eq(["existing@test.com"])
expect(response.parsed_body["invalid"]).to eq(["invalid", author.email])
end
end

def create_params
{
"emails": "#{user.email}, existing@test.com, invalid, #{author.email}"
}
end
end
end
64 changes: 64 additions & 0 deletions spec/requests/api/v1/collaborators_controller/destroy_spec.rb
@@ -0,0 +1,64 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe Api::V1::CollaboratorsController, "#destroy", type: :request do
describe "delete specific collaborator" do
let!(:author) { FactoryBot.create(:user) }
let!(:project) { FactoryBot.create(:project, author: author) }
let!(:collaborator) { FactoryBot.create(:user) }
let!(:collaboration) { FactoryBot.create(:collaboration, user: collaborator, project: project) }

context "when not authenticated" do
before do
delete "/api/v1/projects/#{project.id}/collaborators/#{collaboration.user.id}", as: :json
end

it "returns status unauthenticated" do
expect(response).to have_http_status(401)
expect(response.parsed_body).to have_jsonapi_errors
end
end

context "when authenticated as random user and don't have author_access?" do
before do
token = get_auth_token(FactoryBot.create(:user))
delete "/api/v1/projects/#{project.id}/collaborators/#{collaboration.user.id}",
headers: { "Authorization": "Token #{token}" }, as: :json
end

it "returns status unauthorized" do
expect(response).to have_http_status(403)
expect(response.parsed_body).to have_jsonapi_errors
end
end

context "when authenticated but tries to delete non existent collaborator" do
before do
token = get_auth_token(author)
delete "/api/v1/projects/#{project.id}/collaborators/0",
headers: { "Authorization": "Token #{token}" }, as: :json
end

it "returns status not_found" do
expect(response).to have_http_status(404)
expect(response.parsed_body).to have_jsonapi_errors
end
end

context "when authenticated and has access to delete collaborator" do
before do
token = get_auth_token(author)
delete "/api/v1/projects/#{project.id}/collaborators/#{collaboration.user.id}",
headers: { "Authorization": "Token #{token}" }, as: :json
end

it "deletes collaborator & return status no_content" do
expect { Collaboration.find_by!(user: collaborator, project: project) }.to raise_exception(
ActiveRecord::RecordNotFound
)
expect(response).to have_http_status(204)
end
end
end
end
72 changes: 72 additions & 0 deletions spec/requests/api/v1/collaborators_controller/index_spec.rb
@@ -0,0 +1,72 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe Api::V1::CollaboratorsController, "#index", type: :request do
describe "list all collaborators" do
let!(:author) { FactoryBot.create(:user) }
let!(:public_project) do
FactoryBot.create(:project, author: author, project_access_type: "Public")
end
let!(:private_project) { FactoryBot.create(:project, author: author) }

context "when not authenticated" do
before do
get "/api/v1/projects/#{public_project.id}/collaborators/", as: :json
end

it "returns status unauthorized" do
expect(response).to have_http_status(401)
expect(response.parsed_body).to have_jsonapi_errors
end
end

context "when authorized but invalid/non-existent project" do
before do
token = get_auth_token(FactoryBot.create(:user))
get "/api/v1/projects/0/collaborators/",
headers: { "Authorization": "Token #{token}" }, as: :json
end

it "returns status not_found" do
expect(response).to have_http_status(404)
expect(response.parsed_body).to have_jsonapi_errors
end
end

context "when authorized to fetch project's collaborators which user has view access to" do
before do
# create 3 collaborators for a public project
FactoryBot.create_list(:user, 3).each do |u|
FactoryBot.create(:collaboration, user: u, project: public_project)
end
token = get_auth_token(FactoryBot.create(:user))
get "/api/v1/projects/#{public_project.id}/collaborators/",
headers: { "Authorization": "Token #{token}" }, as: :json
end

it "returns all the collaborators for the given project" do
expect(response).to have_http_status(200)
expect(response).to match_response_schema("users")
expect(response.parsed_body["data"].length).to eq(3)
end
end

context "when fetching project's collaborators which user doesn't have view access to" do
before do
# create 3 collaborators for a private project
FactoryBot.create_list(:user, 3).each do |u|
FactoryBot.create(:collaboration, user: u, project: private_project)
end
token = get_auth_token(FactoryBot.create(:user))
get "/api/v1/projects/#{private_project.id}/collaborators/",
headers: { "Authorization": "Token #{token}" }, as: :json
end

it "returns status unauthorized" do
expect(response).to have_http_status(403)
expect(response.parsed_body).to have_jsonapi_errors
end
end
end
end