Skip to content

Commit

Permalink
Add Collaborators API (#1473)
Browse files Browse the repository at this point in the history
  • Loading branch information
Nitish145 committed Jun 25, 2020
1 parent 8b29336 commit e8bd8cf
Show file tree
Hide file tree
Showing 8 changed files with 330 additions and 3 deletions.
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)
# 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

0 comments on commit e8bd8cf

Please sign in to comment.