extend attachments api to board posts

pull/6319/head
Jens Ulferts 7 years ago
parent 9b2458c23c
commit 441735fb4f
No known key found for this signature in database
GPG Key ID: 3CAA4B1182CF5308
  1. 2
      app/models/board.rb
  2. 4
      lib/api/v3/attachments/attachment_representer.rb
  3. 52
      lib/api/v3/attachments/attachments_by_post_api.rb
  4. 71
      lib/api/v3/posts/post_representer.rb
  5. 53
      lib/api/v3/posts/posts_api.rb
  6. 1
      lib/api/v3/root.rb
  7. 8
      lib/api/v3/utilities/path_helper.rb
  8. 104
      spec/lib/api/v3/posts/post_representer_rendering_spec.rb
  9. 12
      spec/lib/api/v3/utilities/path_helper_spec.rb
  10. 65
      spec/requests/api/v3/attachments/attachment_resource_spec.rb
  11. 148
      spec/requests/api/v3/attachments/attachments_by_post_resource_spec.rb
  12. 82
      spec/requests/api/v3/posts_resource_spec.rb

@ -59,7 +59,7 @@ class Board < ActiveRecord::Base
# Updates topics_count, messages_count and last_message_id attributes for +board_id+
def self.reset_counters!(board_id)
board_id = board_id.to_i
where(['id = ?', board_id])
where(id: board_id)
.update_all("topics_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=#{board_id} AND parent_id IS NULL)," +
" messages_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=#{board_id})," +
" last_message_id = (SELECT MAX(id) FROM #{Message.table_name} WHERE board_id=#{board_id})")

@ -53,6 +53,8 @@ module API
::API::V3::WorkPackages::WorkPackageRepresenter
when WikiPage
::API::V3::WikiPages::WikiPageRepresenter
when Message
::API::V3::Posts::PostRepresenter
end
representer.new(represented.container, current_user: current_user)
@ -66,6 +68,8 @@ module API
%i[work_package subject]
when WikiPage
%i[wiki_page title]
when Message
%i[post subject]
end
::API::Decorators::LinkObject

@ -0,0 +1,52 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
module API
module V3
module Attachments
class AttachmentsByPostAPI < ::API::OpenProjectAPI
resources :attachments do
helpers API::V3::Attachments::AttachmentsByContainerAPI::Helpers
helpers do
def container
post
end
def get_attachment_self_path
api_v3_paths.attachments_by_post(container.id)
end
end
get &API::V3::Attachments::AttachmentsByContainerAPI.read
post &API::V3::Attachments::AttachmentsByContainerAPI.create(%i[edit_messages add_messages])
end
end
end
end
end

@ -0,0 +1,71 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
module API
module V3
module Posts
class PostRepresenter < ::API::Decorators::Single
include API::Decorators::LinkedResource
self_link title_getter: ->(*) { nil }
link :attachments do
{
href: api_v3_paths.attachments_by_post(represented.id)
}
end
link :addAttachment do
next unless current_user_allowed_to(:edit_messages, context: represented.project) ||
current_user_allowed_to(:add_messages, context: represented.project)
{
href: api_v3_paths.attachments_by_post(represented.id),
method: :post
}
end
property :id
property :subject
associated_resource :project,
link: ->(*) do
{
href: api_v3_paths.project(represented.project.id),
title: represented.project.name
}
end
def _type
'Post'
end
end
end
end
end

@ -0,0 +1,53 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
module API
module V3
module Posts
class PostsAPI < ::API::OpenProjectAPI
resources :posts do
helpers do
def post
Message.visible(current_user).find(params[:id])
end
end
route_param :id do
get do
::API::V3::Posts::PostRepresenter.new(post,
current_user: current_user,
embed_links: true)
end
mount ::API::V3::Attachments::AttachmentsByPostAPI
end
end
end
end
end
end

@ -42,6 +42,7 @@ module API
mount ::API::V3::CustomActions::CustomActionsAPI
mount ::API::V3::CustomOptions::CustomOptionsAPI
mount ::API::V3::HelpTexts::HelpTextsAPI
mount ::API::V3::Posts::PostsAPI
mount ::API::V3::Principals::PrincipalsAPI
mount ::API::V3::Priorities::PrioritiesAPI
mount ::API::V3::Projects::ProjectsAPI

@ -59,6 +59,10 @@ module API
download_attachment_path(id, filename)
end
def self.attachments_by_post(id)
"#{post(id)}/attachments"
end
def self.attachments_by_work_package(id)
"#{work_package(id)}/attachments"
end
@ -135,6 +139,10 @@ module API
"#{root}/my_preferences"
end
def self.post(id)
"#{root}/posts/#{id}"
end
def self.principals
"#{root}/principals"
end

@ -0,0 +1,104 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe ::API::V3::Posts::PostRepresenter, 'rendering' do
include ::API::V3::Utilities::PathHelper
let(:message) do
FactoryBot.build_stubbed(:message) do |wp|
allow(wp)
.to receive(:project)
.and_return(project)
end
end
let(:project) { FactoryBot.build_stubbed(:project) }
let(:user) { FactoryBot.build_stubbed(:user) }
let(:representer) do
described_class.create(message, current_user: user, embed_links: true)
end
let(:permissions) { all_permissions }
let(:all_permissions) { %i(edit_messages) }
subject { representer.to_json }
before do
allow(user)
.to receive(:allowed_to?) do |permission, project|
permissions.include?(permission)
end
end
describe '_links' do
it_behaves_like 'has an untitled link' do
let(:link) { 'self' }
let(:href) { api_v3_paths.post message.id }
end
it_behaves_like 'has an untitled link' do
let(:link) { :attachments }
let(:href) { api_v3_paths.attachments_by_post message.id }
end
it_behaves_like 'has a titled link' do
let(:link) { :project }
let(:title) { project.name }
let(:href) { api_v3_paths.project project.id }
end
it_behaves_like 'has an untitled action link' do
let(:link) { :addAttachment }
let(:href) { api_v3_paths.attachments_by_post message.id }
let(:method) { :post }
let(:permission) { :edit_messages }
end
end
describe 'properties' do
it_behaves_like 'property', :_type do
let(:value) { 'Post' }
end
it_behaves_like 'property', :id do
let(:value) { message.id }
end
it_behaves_like 'property', :subject do
let(:value) { message.subject }
end
end
describe '_embedded' do
it 'has project embedded' do
expect(subject)
.to be_json_eql(project.name.to_json)
.at_path('_embedded/project/name')
end
end
end

@ -86,6 +86,12 @@ describe ::API::V3::Utilities::PathHelper do
it_behaves_like 'path', '/attachments/1/file.png'
end
describe '#attachments_by_post' do
subject { helper.attachments_by_post 1 }
it_behaves_like 'api v3 path', '/posts/1/attachments'
end
describe '#attachments_by_work_package' do
subject { helper.attachments_by_work_package 1 }
@ -216,6 +222,12 @@ describe ::API::V3::Utilities::PathHelper do
end
end
describe '#post' do
subject { helper.post 1 }
it_behaves_like 'api v3 path', '/posts/1'
end
describe '#principals' do
subject { helper.principals }

@ -38,11 +38,16 @@ describe 'API v3 Attachment resource', type: :request, content_type: :json do
end
let(:project) { FactoryBot.create(:project, is_public: false) }
let(:role) { FactoryBot.create(:role, permissions: permissions) }
let(:permissions) { %i[view_work_packages view_wiki_pages edit_work_packages edit_wiki_pages] }
let(:permissions) do
%i[view_work_packages view_wiki_pages delete_wiki_pages_attachments
edit_work_packages edit_wiki_pages edit_messages]
end
let(:work_package) { FactoryBot.create(:work_package, author: current_user, project: project) }
let(:attachment) { FactoryBot.create(:attachment, container: container) }
let(:wiki) { FactoryBot.create(:wiki, project: project) }
let(:wiki_page) { FactoryBot.create(:wiki_page, wiki: wiki) }
let(:board) { FactoryBot.create(:board, project: project) }
let(:board_message) { FactoryBot.create(:message, board: board) }
let(:container) { work_package }
before do
@ -53,10 +58,10 @@ describe 'API v3 Attachment resource', type: :request, content_type: :json do
subject(:response) { last_response }
let(:get_path) { api_v3_paths.attachment attachment.id }
%i[wiki_page work_package].each do |attachment_type|
let(:container) { send(attachment_type) }
%i[wiki_page work_package board_message].each do |attachment_type|
context "with a #{attachment_type} attachment" do
let(:container) { send(attachment_type) }
context 'logged in user' do
before do
get get_path
@ -80,7 +85,11 @@ describe 'API v3 Attachment resource', type: :request, content_type: :json do
end
context 'requesting attachments without sufficient permissions' do
let(:permissions) { [] }
if attachment_type == :board_message
let(:current_user) { FactoryBot.create(:user) }
else
let(:permissions) { [] }
end
it_behaves_like 'not found' do
let(:type) { 'Attachment' }
@ -100,37 +109,39 @@ describe 'API v3 Attachment resource', type: :request, content_type: :json do
subject(:response) { last_response }
%i[wiki_page work_package].each do |attachment_type|
let(:container) { send(attachment_type) }
%i[wiki_page work_package board_message].each do |attachment_type|
context "with a #{attachment_type} attachment" do
let(:container) { send(attachment_type) }
context 'with required permissions' do
it 'responds with HTTP No Content' do
expect(subject.status).to eq 204
end
context 'with required permissions' do
it 'responds with HTTP No Content' do
expect(subject.status).to eq 204
end
it 'deletes the attachment' do
expect(Attachment.exists?(attachment.id)).not_to be_truthy
end
it 'deletes the attachment' do
expect(Attachment.exists?(attachment.id)).not_to be_truthy
end
context 'for a non-existent attachment' do
let(:path) { api_v3_paths.attachment 1337 }
context 'for a non-existent attachment' do
let(:path) { api_v3_paths.attachment 1337 }
it_behaves_like 'not found' do
let(:id) { 1337 }
let(:type) { 'Attachment' }
it_behaves_like 'not found' do
let(:id) { 1337 }
let(:type) { 'Attachment' }
end
end
end
end
context 'without required permissions' do
let(:permissions) { %i[view_work_packages view_wiki_pages] }
context 'without required permissions' do
let(:permissions) { %i[view_work_packages view_wiki_pages] }
it 'responds with 403' do
expect(subject.status).to eq 403
end
it 'responds with 403' do
expect(subject.status).to eq 403
end
it 'does not delete the attachment' do
expect(Attachment.exists?(attachment.id)).to be_truthy
it 'does not delete the attachment' do
expect(Attachment.exists?(attachment.id)).to be_truthy
end
end
end
end

@ -0,0 +1,148 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
require 'rack/test'
describe 'API v3 Attachments by post resource', type: :request do
include Rack::Test::Methods
include API::V3::Utilities::PathHelper
include FileHelpers
let(:current_user) do
FactoryBot.create(:user,
member_in_project: project,
member_through_role: role)
end
let(:project) { FactoryBot.create(:project) }
let(:role) { FactoryBot.create(:role, permissions: permissions) }
let(:permissions) { [:view_messages] }
let(:board) { FactoryBot.create(:board, project: project) }
let(:board_message) { FactoryBot.create(:message, board: board) }
subject(:response) { last_response }
before do
allow(User).to receive(:current).and_return current_user
end
describe '#get' do
let(:get_path) { api_v3_paths.attachments_by_post board_message.id }
before do
FactoryBot.create_list(:attachment, 2, container: board_message)
get get_path
end
it 'should respond with 200' do
expect(subject.status).to eq(200)
end
it_behaves_like 'API V3 collection response', 2, 2, 'Attachment'
end
describe '#post' do
let(:permissions) { %i[view_messages edit_messages] }
let(:request_path) { api_v3_paths.attachments_by_post board_message.id }
let(:request_parts) { { metadata: metadata, file: file } }
let(:metadata) { { fileName: 'cat.png' }.to_json }
let(:file) { mock_uploaded_file(name: 'original-filename.txt') }
let(:max_file_size) { 1 } # given in kiB
before do
allow(Setting).to receive(:attachment_max_size).and_return max_file_size.to_s
post request_path, request_parts
end
it 'should respond with HTTP Created' do
expect(subject.status).to eq(201)
end
it 'should return the new attachment' do
expect(subject.body).to be_json_eql('Attachment'.to_json).at_path('_type')
end
it 'ignores the original file name' do
expect(subject.body).to be_json_eql('cat.png'.to_json).at_path('fileName')
end
context 'metadata section is missing' do
let(:request_parts) { { file: file } }
it_behaves_like 'invalid request body', I18n.t('api_v3.errors.multipart_body_error')
end
context 'file section is missing' do
# rack-test won't send a multipart request without a file being present
# however as long as we depend on correctly named sections this test should do just fine
let(:request_parts) { { metadata: metadata, wrongFileSection: file } }
it_behaves_like 'invalid request body', I18n.t('api_v3.errors.multipart_body_error')
end
context 'metadata section is no valid JSON' do
let(:metadata) { '"fileName": "cat.png"' }
it_behaves_like 'parse error'
end
context 'metadata is missing the fileName' do
let(:metadata) { Hash.new.to_json }
it_behaves_like 'constraint violation' do
let(:message) { "fileName #{I18n.t('activerecord.errors.messages.blank')}" }
end
end
context 'file is too large' do
let(:file) { mock_uploaded_file(content: 'a' * 2.kilobytes) }
let(:expanded_localization) do
I18n.t('activerecord.errors.messages.file_too_large', count: max_file_size.kilobytes)
end
it_behaves_like 'constraint violation' do
let(:message) { "File #{expanded_localization}" }
end
end
context 'only allowed to add messages, but no edit permission' do
let(:permissions) { %i[view_messages add_messages] }
it 'should respond with HTTP Created' do
expect(subject.status).to eq(201)
end
end
context 'only allowed to view messages' do
let(:permissions) { [:view_messages] }
it_behaves_like 'unauthorized access'
end
end
end

@ -0,0 +1,82 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
require 'rack/test'
describe 'API v3 posts resource', type: :request do
include Rack::Test::Methods
include API::V3::Utilities::PathHelper
let(:current_user) do
FactoryBot.create(:user, member_in_project: project, member_through_role: role)
end
let(:board) { FactoryBot.create(:board, project: project) }
let(:message) { FactoryBot.create(:message, board: board) }
let(:project) { FactoryBot.create(:project) }
let(:role) { FactoryBot.create(:role, permissions: permissions) }
let(:permissions) { %i(view_messages) }
subject(:response) { last_response }
before do
login_as(current_user)
end
describe 'GET /api/v3/posts/:id' do
let(:path) { api_v3_paths.post(message.id) }
before do
get path
end
it 'returns 200 OK' do
expect(subject.status)
.to eql(200)
end
it 'returns the message page' do
expect(subject.body)
.to be_json_eql('Post'.to_json)
.at_path('_type')
expect(subject.body)
.to be_json_eql(message.id.to_json)
.at_path('id')
end
context 'when lacking permissions' do
let(:current_user) { FactoryBot.create(:user) }
it 'returns 404 NOT FOUND' do
expect(subject.status)
.to eql(404)
end
end
end
end
Loading…
Cancel
Save