finalized specs adjustments

This commit is contained in:
Jonas Jabari
2023-05-19 17:18:23 +02:00
parent 3ccdeb510e
commit a41fe0c64c
18 changed files with 814 additions and 230 deletions
+1 -1
View File
@@ -368,7 +368,7 @@ en:
copy_url_label: "Copy URL"
ical_generation_error_text: "An error occured while generating the iCal URL"
copy_url_success_text: "URL copied to clipboard"
copy_url_error_text: "couldn't be copied to clipboard"
copy_url_error_text: "Due to browser errors, the URL couldn't be copied to clipboard. Please copy it manually:"
label_activate: "Activate"
label_assignee: 'Assignee'
@@ -116,17 +116,25 @@ export class QueryGetIcalUrlModalComponent extends OpModalComponent implements O
}
public copyUrlAndCloseModal(url:string):void {
void navigator.clipboard.writeText(url)
if(!navigator.clipboard) {
// fallback for browsers that don't support clipboard API at all
this.toastService.addWarning(
this.I18n.t('js.ical_sharing_modal.copy_url_error_text') + " " + url
);
} else {
void navigator.clipboard.writeText(url)
.then(() => {
this.toastService.addSuccess(this.text.copy_success_text);
this.closeMe();
})
.catch(() => {
// e.g. browser permission errors
this.toastService.addError(
url + " " + this.I18n.t('js.ical_sharing_modal.copy_url_error_text')
// fallback when running into e.g. browser permission errors
this.toastService.addWarning(
this.I18n.t('js.ical_sharing_modal.copy_url_error_text') + " " + url
);
});
}
this.closeMe();
}
public generateAndCopyUrl(event:any):void {
@@ -36,6 +36,7 @@ module API
class QueryIcalUrlAPI < ::API::OpenProjectAPI
namespace :ical_url do
# TODO: introduce OpenProject::Configuration.ical_subscriptions_enabled configuration
# before do
# raise API::Errors::NotFound unless OpenProject::Configuration.ical_subscriptions_enabled?
# end
@@ -46,9 +47,7 @@ module API
post do
authorize_by_policy(:share_via_ical)
# TODO: write test for this
# TODO: currently the generated URL points to controller action in calendar module
# QUESTION: currently the generated URL points to controller action in calendar module
# correct approach? or should it be implemented as a API here?
call = ::Calendar::GenerateICalUrl.new.call(
user: current_user,
@@ -62,7 +61,8 @@ module API
end
status 201
# QUESTION: is it ok to use an OpenStruct here?
QueryICalUrlRepresenter.new(
OpenStruct.new(ical_url: call.result, query: @query),
current_user:
@@ -35,6 +35,10 @@ module API
module ICalUrl
class QueryICalUrlRepresenter < ::API::Decorators::Single
def initialize(model, *_)
super(model, current_user: nil)
end
link :self do
{
href: api_v3_paths.query_ical_url(represented.query.id),
@@ -59,7 +63,7 @@ module API
end
def _type
'QueryIcalUrl'
'QueryICalUrl'
end
end
end
@@ -29,19 +29,15 @@
module Calendar
class ResolveWorkPackagesService < ::BaseServices::BaseCallable
def perform(query:)
unless query.nil?
# TODO: check if the includes makes sense here in order to avoid n+1 queries
work_packages = query.results.work_packages.includes(
:project, :assigned_to, :author, :priority, :status
)
work_packages_with_dates = work_packages
.where.not(start_date: nil, due_date: nil)
raise ActiveRecord::RecordNotFound if query.nil?
ServiceResult.success(result: work_packages_with_dates)
else
# TODO: raise specific error
raise ActiveRecord::RecordNotFound
end
work_packages = query.results.work_packages.includes(
:project, :assigned_to, :author, :priority, :status
)
work_packages_with_dates = work_packages
.where.not(start_date: nil, due_date: nil)
ServiceResult.success(result: work_packages_with_dates)
end
end
end
@@ -36,6 +36,28 @@ describe Calendar::ICalController do
member_with_permissions: sufficient_permissions)
end
let(:sufficient_permissions) { %i[view_work_packages share_calendars] }
let(:insufficient_permissions) { %i[view_work_packages] }
let(:work_package_with_due_date) do
create(:work_package, project:,
due_date: Time.zone.today + 7.days)
end
let(:work_package_with_start_date) do
create(:work_package, project:,
start_date: Time.zone.today + 14.days)
end
let(:work_package_with_start_and_due_date) do
create(:work_package, project:,
start_date: Date.tomorrow,
due_date: Time.zone.today + 7.days)
end
let!(:work_packages) do
[
work_package_with_due_date,
work_package_with_start_date,
work_package_with_start_and_due_date
]
end
let(:query) do
create(:query,
project:,
@@ -43,25 +65,48 @@ describe Calendar::ICalController do
public: false)
end
let(:valid_ical_token_value) do
Token::ICal.create_and_return_value(user, query)
Token::ICal.create_and_return_value(user, query, "Some Token Name")
end
# the ical urls are intended to be used without a logged in user from a calendar client app
# before { login_as(user) }
describe '#show' do
shared_examples_for 'ical#show' do |expected|
shared_examples_for 'success' do
subject { response }
if expected == :success
it { is_expected.to be_successful }
end
if expected == :failure
it { is_expected.not_to be_successful }
it { is_expected.to be_successful }
it 'returns a valid ical file' do
expect(response.headers['Content-Type']).to eq('text/calendar')
expect(response.headers['Content-Disposition']).to eq(
"attachment; filename=\"openproject_calendar_#{DateTime.now.to_i}.ics\"; filename*=UTF-8''openproject_calendar_#{DateTime.now.to_i}.ics"
)
expect(subject.body).to match(/BEGIN:VCALENDAR/)
expect(subject.body).to match(/END:VCALENDAR/)
work_packages.each do |work_package|
expect(subject.body).to include(work_package.subject)
end
end
end
context 'with valid params' do
shared_examples_for 'failure' do
subject { response }
it { is_expected.not_to be_successful }
it 'does not return a valid ical file' do
expect(subject.body).not_to match(/BEGIN:VCALENDAR/)
expect(subject.body).not_to match(/END:VCALENDAR/)
work_packages.each do |work_package|
expect(subject.body).not_to include(work_package.subject)
end
end
end
context 'with valid params and permissions when targeting own query' do
before do
get :show, params: {
project_id: project.id,
@@ -70,7 +115,95 @@ describe Calendar::ICalController do
}
end
it_behaves_like 'ical#show', :success
it_behaves_like 'success'
end
context 'with valid params and permissions when targeting a public query of somebody else' do
let(:user2) do
create(:user,
member_in_project: project,
member_with_permissions: sufficient_permissions)
end
let(:query2) do
create(:query,
project:,
user: user2,
public: true)
end
let(:valid_ical_token_value) do
Token::ICal.create_and_return_value(user, query2, "Some Token Name")
end
before do
get :show, params: {
project_id: project.id,
id: query2.id,
ical_token: valid_ical_token_value
}
end
it_behaves_like 'success'
end
context 'with valid params and permissions when targeting a privat query of somebody else' do
let(:user2) do
create(:user,
member_in_project: project,
member_with_permissions: sufficient_permissions)
end
let(:query2) do
create(:query,
project:,
user: user2,
public: false)
end
let(:valid_ical_token_value) do
Token::ICal.create_and_return_value(user, query2, "Some Token Name")
end
before do
get :show, params: {
project_id: project.id,
id: query2.id,
ical_token: valid_ical_token_value
}
end
it_behaves_like 'failure'
end
context 'with valid params and permissions when not part of the project (anymore)' do
let(:project2) { create(:project) }
let(:user) do
create(:user,
member_in_project: project2,
member_with_permissions: sufficient_permissions)
end
before do
get :show, params: {
project_id: project.id,
id: query.id,
ical_token: valid_ical_token_value
}
end
it_behaves_like 'failure'
end
context 'with valid params and missing permissions' do
let(:user) do
create(:user,
member_in_project: project,
member_with_permissions: insufficient_permissions)
end
before do
get :show, params: {
project_id: project.id,
id: query.id,
ical_token: valid_ical_token_value
}
end
it_behaves_like 'failure'
end
context 'with invalid token' do
@@ -82,7 +215,7 @@ describe Calendar::ICalController do
}
end
it_behaves_like 'ical#show', :failure
it_behaves_like 'failure'
end
context 'with invalid query' do
@@ -94,7 +227,7 @@ describe Calendar::ICalController do
}
end
it_behaves_like 'ical#show', :failure
it_behaves_like 'failure'
end
context 'with invalid project' do
@@ -108,7 +241,7 @@ describe Calendar::ICalController do
# TODO: the project id is actually irrelevant - the query id is enough
# should the project id still be used in the ical url anyways?
it_behaves_like 'ical#show', :success
it_behaves_like 'success'
end
end
end
@@ -91,18 +91,18 @@ describe 'Calendar sharing via ical', js: true do
# expect disabled sharing menu item
within "#settingsDropdown" do
# expect(page).to have_button("Share iCalendar", disabled: true) # disabled selector not working
expect(page).to have_selector(".menu-item.inactive", text: "Share iCalendar")
page.click_button("Share iCalendar")
# expect(page).to have_button("Subscribe to iCalendar", disabled: true) # disabled selector not working
expect(page).to have_selector(".menu-item.inactive", text: "Subscribe to iCalendar")
page.click_button("Subscribe to iCalendar")
# modal should not be shown
expect(page).not_to have_selector('.spot-modal--header', text: "Share iCalendar")
expect(page).not_to have_selector('.spot-modal--header', text: "Subscribe to iCalendar")
end
end
end
context 'on persisted calendar query' do
it 'shows sharing menu item and sharing modal if clicked' do
before do
saved_query
visit project_calendars_path(project)
@@ -111,8 +111,10 @@ describe 'Calendar sharing via ical', js: true do
click_link saved_query.name
end
loading_indicator_saveguard
loading_indicator_saveguard
end
it 'shows an active menu item' do
# wait for settings button to become visible
expect(page).to have_selector("#work-packages-settings-button")
@@ -121,21 +123,91 @@ describe 'Calendar sharing via ical', js: true do
# expect disabled sharing menu item
within "#settingsDropdown" do
expect(page).to have_selector(".menu-item", text: "Share iCalendar")
page.click_button("Share iCalendar")
expect(page).to have_selector(".menu-item", text: "Subscribe to iCalendar")
end
end
it 'shows a sharing modal' do
open_sharing_modal
expect(page).to have_selector('.spot-modal--header', text: "Share iCalendar")
expect(page).to have_selector('.spot-modal--header', text: "Subscribe to iCalendar")
end
it 'closes the sharing modal when closed by user by clicking the close button' do
open_sharing_modal
expect(page).to have_selector('.spot-modal--header', text: "Subscribe to iCalendar")
click_button "Cancel"
expect(page).not_to have_selector('.spot-modal--header', text: "Subscribe to iCalendar")
end
# it 'closes the sharing modal when closed by user by hitting escape' do
# open_sharing_modal
# expect(page).to have_selector('.spot-modal--header', text: "Subscribe to iCalendar")
# page.find(:css, '.spot-modal--header').send_keys :escape
# expect(page).not_to have_selector('.spot-modal--header', text: "Subscribe to iCalendar")
# end
it 'successfully requests a new tokenized iCalendar URL when a unique name is provided' do
open_sharing_modal
fill_in "Name", with: "A token name"
click_button "Copy URL"
# Not working in test env, probably due to missing clipboard permissions of the headless browser
# implicitly testing for success -> modal is closed and fallback message is shown
expect(page).not_to have_selector('.spot-modal--header', text: "Subscribe to iCalendar")
expect(page).to have_content("/projects/#{saved_query.project.id}/calendars/#{saved_query.id}/ical?ical_token=")
# explictly testing for success message is not working in test env, probably
# due to missing clipboard permissions of the headless browser
#
# expect(page).to have_content("URL copied to clipboard")
# TODO: Not able to test if the URL was actuall copied to the clipboard
# Tried following without success
# https://copyprogramming.com/howto/emulating-a-clipboard-copy-paste-with-selinum-capybara
end
it 'validates the presence of a name' do
open_sharing_modal
# fill_in "Name", with: "A token name"
click_button "Copy URL"
# modal is still shown and error message is shown
expect(page).to have_selector('.spot-modal--header', text: "Subscribe to iCalendar")
expect(page).to have_content("Name is mandatory")
end
it 'validates the uniqueness of a name' do
open_sharing_modal
fill_in "Name", with: "A token name"
click_button "Copy URL"
expect(page).not_to have_selector('.spot-modal--header', text: "Subscribe to iCalendar")
expect(page).to have_content("/projects/#{saved_query.project.id}/calendars/#{saved_query.id}/ical?ical_token=")
# do the same thing again, now expect validation error
open_sharing_modal
fill_in "Name", with: "A token name" # same name for same user and same query -> not allowed
click_button "Copy URL"
# modal is still shown and error message is shown
expect(page).to have_selector('.spot-modal--header', text: "Subscribe to iCalendar")
expect(page).to have_content("Name is already in use")
end
end
end
@@ -159,14 +231,30 @@ describe 'Calendar sharing via ical', js: true do
# expect disabled sharing menu item
within "#settingsDropdown" do
# expect(page).to have_button("Share iCalendar", disabled: true) # disabled selector not working
expect(page).to have_selector(".menu-item.inactive", text: "Share iCalendar")
page.click_button("Share iCalendar")
# expect(page).to have_button("Subscribe to iCalendar", disabled: true) # disabled selector not working
expect(page).to have_selector(".menu-item.inactive", text: "Subscribe to iCalendar")
page.click_button("Subscribe to iCalendar")
# modal should not be shown
expect(page).not_to have_selector('.spot-modal--header', text: "Share iCalendar")
expect(page).not_to have_selector('.spot-modal--header', text: "Subscribe to iCalendar")
end
end
end
end
# helper methods
def open_sharing_modal
# wait for settings button to become visible
expect(page).to have_selector("#work-packages-settings-button")
# click on settings button
page.find_by_id('work-packages-settings-button').click
# expect disabled sharing menu item
within "#settingsDropdown" do
expect(page).to have_selector(".menu-item", text: "Subscribe to iCalendar")
page.click_button("Subscribe to iCalendar")
end
end
end
@@ -48,11 +48,4 @@ describe Calendar::CalendarsController do
id: '2',
project_id: '1')
end
it do
expect(get('/projects/1/calendars/2/ical')).to route_to(controller: 'calendar/ical',
action: 'ical',
id: '2',
project_id: '1')
end
end
@@ -45,7 +45,7 @@ describe Calendar::CreateICalService, type: :model do
end
let(:work_package_with_due_date_and_assignee) do
create(:work_package, project:,
due_date: Time.zone.today + 30.days, assigned_to: user)
due_date: Time.zone.today + 60.days, assigned_to: user)
end
let(:work_packages) do
[
@@ -93,7 +93,7 @@ describe Calendar::CreateICalService, type: :model do
DTEND;VALUE=DATE:#{(work_package_with_due_date.due_date + 1.day).strftime('%Y%m%d')}
DESCRIPTION:Project: #{project.name}\nType: None\nStatus: #{work_package_with_due_date.status.name}\nAssignee: \nPriority: #{work_package_with_due_date.priority.name}\n\nDescription:\n #{work_package_with_due_date.description}
LOCATION:http://localhost:3000/work_packages/#{work_package_with_due_date.id}
ORGANIZER:Bob Bobbit
ORGANIZER:#{work_package_with_due_date.author&.name}
SUMMARY:#{work_package_with_due_date.name}
END:VEVENT
BEGIN:VEVENT
@@ -103,7 +103,7 @@ describe Calendar::CreateICalService, type: :model do
DTEND;VALUE=DATE:#{(work_package_with_start_date.start_date + 1.day).strftime('%Y%m%d')}
DESCRIPTION:Project: #{project.name}\nType: None\nStatus: #{work_package_with_start_date.status.name}\nAssignee: \nPriority: #{work_package_with_start_date.priority.name}\n\nDescription:\n #{work_package_with_start_date.description}
LOCATION:http://localhost:3000/work_packages/#{work_package_with_start_date.id}
ORGANIZER:Bob Bobbit
ORGANIZER:#{work_package_with_start_date.author&.name}
SUMMARY:#{work_package_with_start_date.name}
END:VEVENT
BEGIN:VEVENT
@@ -113,7 +113,7 @@ describe Calendar::CreateICalService, type: :model do
DTEND;VALUE=DATE:#{(work_package_with_start_and_due_date.due_date + 1.day).strftime('%Y%m%d')}
DESCRIPTION:Project: #{project.name}\nType: None\nStatus: #{work_package_with_start_and_due_date.status.name}\nAssignee: \nPriority: #{work_package_with_start_and_due_date.priority.name}\n\nDescription:\n #{work_package_with_start_and_due_date.description}
LOCATION:http://localhost:3000/work_packages/#{work_package_with_start_and_due_date.id}
ORGANIZER:Bob Bobbit
ORGANIZER:#{work_package_with_start_and_due_date.author&.name}
SUMMARY:#{work_package_with_start_and_due_date.name}
END:VEVENT
BEGIN:VEVENT
@@ -123,7 +123,7 @@ describe Calendar::CreateICalService, type: :model do
DTEND;VALUE=DATE:#{(work_package_with_due_date_and_assignee.due_date + 1.day).strftime('%Y%m%d')}
DESCRIPTION:Project: #{project.name}\nType: None\nStatus: #{work_package_with_due_date_and_assignee.status.name}\nAssignee: #{work_package_with_due_date_and_assignee.assigned_to.name}\nPriority: #{work_package_with_due_date_and_assignee.priority.name}\n\nDescription:\n #{work_package_with_due_date_and_assignee.description}
LOCATION:http://localhost:3000/work_packages/#{work_package_with_due_date_and_assignee.id}
ORGANIZER:Bob Bobbit
ORGANIZER:#{work_package_with_due_date_and_assignee.author&.name}
SUMMARY:#{work_package_with_due_date_and_assignee.name}
ATTENDEE:#{work_package_with_due_date_and_assignee.assigned_to.name}
END:VEVENT
@@ -29,89 +29,133 @@
require 'spec_helper'
describe Calendar::ResolveAndAuthorizeQueryService, type: :model do
# let(:user3_not_member) do
# create(:user,
# member_in_project: nil)
# end
let(:sufficient_permissions) { %i[view_work_packages share_calendars] }
let(:insufficient_permissions) { %i[view_work_packages] }
let(:project) { create(:project) }
let(:user) do
create(:user,
member_in_project: project,
member_with_permissions: sufficient_permissions)
end
let(:query1) do
create(:query,
project:,
user: user,
public: false) # privat query
end
let(:query2) do
create(:query,
project:,
user: user,
public: true) # public query
end
let(:ical_token_instance_for_query1) do
Token::ICal.create(user: user,
ical_token_query_assignment_attributes: { query: query1, name: "My Token", user_id: user.id }
)
end
let(:ical_token_instance_for_query2) do
Token::ICal.create(user: user,
ical_token_query_assignment_attributes: { query: query2, name: "My Token", user_id: user.id }
)
end
let(:instance) do
described_class.new
end
shared_examples 'not found' do
it 'raises ActiveRecord::RecordNotFound' do
expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context 'if user is authenticated to read from query and user is permitted to use ical sharing' do
let(:user) do
create(:user,
member_in_project: project,
member_with_permissions: sufficient_permissions)
end
let(:query1) do
create(:query,
project:,
user: user,
public: false)
end
let(:query2) do
create(:query,
project:,
user: user,
public: false)
end
let(:ical_token_instance_for_query1) do
Token::ICal.create(user: user,
ical_token_query_assignment_attributes: { query: query1, name: "My Token" }
)
end
let(:ical_token_instance_for_query2) do
Token::ICal.create(user: user,
ical_token_query_assignment_attributes: { query: query2, name: "My Token" }
)
end
context 'if ical token belongs to query' do
subject do
instance.call(
query_id: query1.id,
ical_token_instance: ical_token_instance_for_query1
)
context 'if user owns the query which is private' do
subject do
instance.call(
query_id: query1.id,
ical_token_instance: ical_token_instance_for_query1
)
end
it 'returns the query as result' do
expect(subject.result)
.to eq query1
end
it 'is a success' do
expect(subject)
.to be_success
end
end
it 'returns the query as result' do
expect(subject.result)
.to eq query1
end
context 'if user owns the query which is public' do
subject do
instance.call(
query_id: query2.id,
ical_token_instance: ical_token_instance_for_query2
)
end
it 'is a success' do
expect(subject)
.to be_success
it 'returns the query as result' do
expect(subject.result)
.to eq query2
end
it 'is a success' do
expect(subject)
.to be_success
end
end
end
context 'if ical token does NOT belong to query' do
subject do
instance.call(
query_id: query1.id,
ical_token_instance: ical_token_instance_for_query2
)
context 'if user owns the query which is private' do
subject do
instance.call(
query_id: query1.id,
ical_token_instance: ical_token_instance_for_query2
)
end
it_behaves_like "not found"
end
it 'raises ActiveRecord::RecordNotFound' do
expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
context 'if user owns the query which is public' do
subject do
instance.call(
query_id: query2.id,
ical_token_instance: ical_token_instance_for_query1
)
end
it_behaves_like "not found"
end
end
context 'if ical token is nil' do
subject do
instance.call(
query_id: query1.id,
ical_token_instance: nil
)
context 'if query is private' do
subject do
instance.call(
query_id: query1.id,
ical_token_instance: nil
)
end
it_behaves_like "not found"
end
it 'raises ActiveRecord::RecordNotFound' do
expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
context 'if query is public' do
subject do
instance.call(
query_id: query2.id,
ical_token_instance: nil
)
end
it_behaves_like "not found"
end
end
end
@@ -122,57 +166,57 @@ describe Calendar::ResolveAndAuthorizeQueryService, type: :model do
member_in_project: project,
member_with_permissions: insufficient_permissions)
end
let(:query1) do
create(:query,
project:,
user: user,
public: false)
end
let(:query2) do
create(:query,
project:,
user: user,
public: false)
end
let(:ical_token_instance_for_query1) do
Token::ICal.create(user: user,
ical_token_query_assignment_attributes: { query: query1, name: "My Token" }
)
end
let(:ical_token_instance_for_query2) do
Token::ICal.create(user: user,
ical_token_query_assignment_attributes: { query: query2, name: "My Token" }
)
end
context 'if ical token belongs to query' do
subject do
instance.call(
query_id: query1.id,
ical_token_instance: ical_token_instance_for_query1
)
context 'if user owns the query which is private' do
subject do
instance.call(
query_id: query1.id,
ical_token_instance: ical_token_instance_for_query1
)
end
it_behaves_like "not found"
end
it 'raises ActiveRecord::RecordNotFound' do
expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
context 'if user owns the query which is public' do
subject do
instance.call(
query_id: query2.id,
ical_token_instance: ical_token_instance_for_query2
)
end
it_behaves_like "not found"
end
end
context 'if ical token does NOT belong to query' do
subject do
instance.call(
query_id: query1.id,
ical_token_instance: ical_token_instance_for_query2
)
context 'if user owns the query which is private' do
subject do
instance.call(
query_id: query1.id,
ical_token_instance: ical_token_instance_for_query2
)
end
it_behaves_like "not found"
end
it 'raises ActiveRecord::RecordNotFound' do
expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
context 'if user owns the query which is public' do
subject do
instance.call(
query_id: query2.id,
ical_token_instance: ical_token_instance_for_query1
)
end
it_behaves_like "not found"
end
end
end
context 'if user is not authenticated to read from query' do
context 'if user does not own the query' do
let(:user1) do
create(:user,
member_in_project: project,
@@ -183,50 +227,99 @@ describe Calendar::ResolveAndAuthorizeQueryService, type: :model do
member_in_project: project,
member_with_permissions: sufficient_permissions)
end
let(:query1) do
let(:private_query1_of_user1) do
create(:query,
project:,
user: user1,
public: false)
end
let(:public_query2_of_user1) do
create(:query,
project:,
user: user1,
public: true)
end
let(:ical_token_instance_of_user_2_for_query1) do
Token::ICal.create(user: user2,
ical_token_query_assignment_attributes: { query: query1, name: "My Token" }
ical_token_query_assignment_attributes: {
query: private_query1_of_user1, name: "My Token", user_id: user2.id
}
)
end
let(:ical_token_instance_of_user_2_for_query2) do
Token::ICal.create(user: user2,
ical_token_query_assignment_attributes: {
query: public_query2_of_user1, name: "My Token", user_id: user2.id
}
)
end
context 'if ical token belongs to query' do
context 'if query is private' do
subject do
instance.call(
query_id: query1.id,
query_id: private_query1_of_user1.id,
ical_token_instance: ical_token_instance_of_user_2_for_query1
)
end
it 'raises ActiveRecord::RecordNotFound' do
expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
it_behaves_like "not found"
end
context 'if query is public' do
subject do
instance.call(
query_id: public_query2_of_user1.id,
ical_token_instance: ical_token_instance_of_user_2_for_query2
)
end
it 'returns the query as result' do
expect(subject.result)
.to eq public_query2_of_user1
end
it 'is a success' do
expect(subject)
.to be_success
end
end
end
context 'if query id is invalid or nil' do
context 'if user is not member of the project (anymore)' do
let(:project2) { create(:project) }
let(:user) do
create(:user,
member_in_project: project,
member_in_project: project2,
member_with_permissions: sufficient_permissions)
end
let(:query1) do
create(:query,
project:,
user: user,
public: false)
end
let(:ical_token_instance_for_query1) do
Token::ICal.create(user: user,
ical_token_query_assignment_attributes: { query: query1, name: "My Token" }
)
# queries (privat and public) owned by user
# but user is not part of the project anymore
context 'if query is private' do
subject do
instance.call(
query_id: query1.id,
ical_token_instance: ical_token_instance_for_query1
)
end
it_behaves_like "not found"
end
context 'if query is public' do
subject do
instance.call(
query_id: query2.id,
ical_token_instance: ical_token_instance_for_query2
)
end
it_behaves_like "not found"
end
end
context 'if query id is invalid or nil' do
context 'if query id is invalid' do
subject do
instance.call(
@@ -235,9 +328,7 @@ describe Calendar::ResolveAndAuthorizeQueryService, type: :model do
)
end
it 'raises ActiveRecord::RecordNotFound' do
expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
end
it_behaves_like "not found"
end
context 'if query id is nil' do
@@ -248,9 +339,7 @@ describe Calendar::ResolveAndAuthorizeQueryService, type: :model do
)
end
it 'raises ActiveRecord::RecordNotFound' do
expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
end
it_behaves_like "not found"
end
end
end
+31 -11
View File
@@ -315,20 +315,40 @@ describe MyController do
# in this context all ical tokens of a user should be reverted at once
# this invalidates all previously generated ical urls, which is the intention
context 'with existing keys' do
let!(:project) { create(:project) }
let!(:query1) { create(:query, project: project) }
let!(:query2) { create(:query, project: project) }
let!(:key1_of_query1) { Token::ICal.create user:, query: query1}
let!(:key2_of_query1) { Token::ICal.create user:, query: query1}
let!(:key1_of_query2) { Token::ICal.create user:, query: query2}
let!(:key2_of_query2) { Token::ICal.create user:, query: query2}
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:query1) { create(:query, project: project) }
let(:query2) { create(:query, project: project) }
let!(:ical_token_for_query1) do
Token::ICal.create(user:,
ical_token_query_assignment_attributes: { query: query1, name: "Some Token Name", user_id: user.id }
)
end
let!(:ical_token_for_query1_2) do
Token::ICal.create(user:,
ical_token_query_assignment_attributes: { query: query1, name: "Some Other Token Name", user_id: user.id }
)
end
let!(:ical_token_for_query2_1) do
Token::ICal.create(user:,
ical_token_query_assignment_attributes: { query: query2, name: "Some Token Name", user_id: user.id }
)
end
it 'revoke specific ical tokens' do
expect(user.ical_tokens).to contain_exactly(
ical_token_for_query1, ical_token_for_query1_2, ical_token_for_query2_1
)
it 'revokes all existing keys' do
expect(user.ical_tokens).to contain_exactly(key1_of_query1, key2_of_query1, key1_of_query2, key2_of_query2)
delete :revoke_ical_token, params: { id: ical_token_for_query1_2.id }
delete :revoke_all_ical_tokens_of_query, params: { query_id: query2.id }
expect(user.ical_tokens.reload).to contain_exactly(
ical_token_for_query1, ical_token_for_query2_1
)
expect(user.ical_tokens.reload).to contain_exactly(key1_of_query1, key2_of_query1)
expect(user.ical_tokens.reload).not_to contain_exactly(
ical_token_for_query2_1
)
expect(flash[:info]).to be_present
expect(flash[:error]).not_to be_present
+40 -25
View File
@@ -162,66 +162,81 @@ describe 'my', js: true do
expect(page).to have_content 'iCalendar token(s) not present'
end
new_ical_token_for_query1 = Token::ICal.create(user:, query: query1)
new_ical_token_for_query1 = Token::ICal.create(user:,
ical_token_query_assignment_attributes: { query: query1, name: "Some Token Name", user_id: user.id }
)
visit my_access_token_path
expect(page).not_to have_content 'iCalendar token(s) not present'
within(:xpath, "//tr[contains(.,'iCalendar token(s) for calendar \"#{query1.name}\"')]") do
expect(page).to have_content "iCalendar token(s) for calendar \"#{query1.name}\" of project \"#{project.name}\""
expect(page).to have_content "#{I18n.l(new_ical_token_for_query1.created_at, format: :time)} (latest)"
expected_content = "iCalendar token \"#{new_ical_token_for_query1.ical_token_query_assignment.name}\" for \"#{query1.name}\" in \"#{project.name}\""
within(:xpath, "//tr[contains(.,'#{expected_content}')]") do
expect(page).to have_content expected_content
expect(page).to have_content "#{I18n.l(new_ical_token_for_query1.created_at, format: :time)}"
end
Timecop.travel(1.minute.from_now) do
another_new_ical_token_for_query1 = Token::ICal.create(user:, query: query1)
another_new_ical_token_for_query1 = Token::ICal.create(user:,
ical_token_query_assignment_attributes: { query: query1, name: "Some Other Token Name", user_id: user.id }
)
visit my_access_token_path
expect(page).not_to have_content 'iCalendar token(s) not present'
expected_content = "iCalendar token \"#{another_new_ical_token_for_query1.ical_token_query_assignment.name}\" for \"#{query1.name}\" in \"#{project.name}\""
within(:xpath, "//tr[contains(.,'iCalendar token(s) for calendar \"#{query1.name}\"')]") do
expect(page).to have_content "iCalendar token(s) for calendar \"#{query1.name}\" of project \"#{project.name}\""
expect(page).to have_content "#{I18n.l(another_new_ical_token_for_query1.created_at, format: :time)} (latest)"
within(:xpath, "//tr[contains(.,'#{expected_content}')]") do
expect(page).to have_content expected_content
expect(page).to have_content "#{I18n.l(another_new_ical_token_for_query1.created_at, format: :time)}"
end
end
Timecop.travel(2.minutes.from_now) do
new_ical_token_for_query2 = Token::ICal.create(user:, query: query2)
new_ical_token_for_query2 = Token::ICal.create(user:,
ical_token_query_assignment_attributes: { query: query2, name: "Some Token Name", user_id: user.id }
)
visit my_access_token_path
expect(page).not_to have_content 'iCalendar token(s) not present'
expected_content = "iCalendar token \"#{new_ical_token_for_query2.ical_token_query_assignment.name}\" for \"#{query2.name}\" in \"#{project.name}\""
within(:xpath, "//tr[contains(.,'iCalendar token(s) for calendar \"#{query2.name}\"')]") do
expect(page).to have_content "iCalendar token(s) for calendar \"#{query2.name}\" of project \"#{project.name}\""
expect(page).to have_content "#{I18n.l(new_ical_token_for_query2.created_at, format: :time)} (latest)"
within(:xpath, "//tr[contains(.,'#{expected_content}')]") do
expect(page).to have_content expected_content
expect(page).to have_content "#{I18n.l(new_ical_token_for_query2.created_at, format: :time)}"
end
end
end
it 'in Access Tokens they can revoke all existing Ical tokens' do
2.times do
Token::ICal.create(user:, query: query1)
end
Token::ICal.create(user:, query: query2)
new_ical_token_for_query1 = Token::ICal.create(user:,
ical_token_query_assignment_attributes: { query: query1, name: "Some Token Name", user_id: user.id }
)
another_new_ical_token_for_query1 = Token::ICal.create(user:,
ical_token_query_assignment_attributes: { query: query1, name: "Some Other Token Name", user_id: user.id }
)
new_ical_token_for_query2 = Token::ICal.create(user:,
ical_token_query_assignment_attributes: { query: query2, name: "Some Token Name", user_id: user.id }
)
expect(user.ical_tokens.count).to eq 3
visit my_access_token_path
within(:xpath, "//tr[contains(.,'iCalendar token(s) for calendar \"#{query1.name}\"')]") do
expect(page).to have_content "Revoke all"
expected_content = "iCalendar token \"#{new_ical_token_for_query1.ical_token_query_assignment.name}\" for \"#{query1.name}\" in \"#{project.name}\""
within(:xpath, "//tr[contains(.,'#{expected_content}')]") do
expect(page).to have_content "Revoke"
click_link "Revoke"
end
find(:xpath, "//tr[contains(.,'iCalendar token(s) for calendar \"#{query1.name}\"')]/td/a", text: 'Revoke all').click
# find(:xpath, "//tr[contains(.,'iCalendar token(s) for \"#{query1.name}\"')]/td/a", text: 'Revoke all').click
expect(page).to have_content "All iCalendar tokens for calendar #{query1.name} of project #{project.name} have been revoked."
expect(page).to have_content "iCalendar token \"#{new_ical_token_for_query1.ical_token_query_assignment.name}\" for calendar \"#{query1.name}\" of project \"#{project.name}\" has been revoked."
expect(page).not_to have_content "iCalendar token(s) for calendar \"#{query1.name}\" of project \"#{project.name}\""
expect(page).to have_content "iCalendar token(s) for calendar \"#{query2.name}\" of project \"#{project.name}\""
expect(page).not_to have_content expected_content
expect(user.ical_tokens.count).to eq 1
expect(user.ical_tokens.count).to eq 2
end
end
end
@@ -0,0 +1,69 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# 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-2013 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 COPYRIGHT and LICENSE files for more details.
#++
require 'spec_helper'
describe API::V3::Queries::ICalUrl::QueryICalUrlRepresenter do
include API::V3::Utilities::PathHelper
let(:query) { build_stubbed(:query) }
let(:mocked_ical_url) { 'https://community.openproject.org/projects/3/calendars/46/ical?ical_token=66a44f91a18ad0355cfad77c319ef5ee2973291499fb8e44a220885f9124d2d2' }
let(:representer) do described_class.new(
OpenStruct.new(ical_url: mocked_ical_url, query: query)
) end
subject { representer.to_json }
describe 'generation' do
describe '_links' do
it_behaves_like 'has an untitled link' do
let(:link) { 'self' }
let(:href) { api_v3_paths.query_ical_url(query.id) }
let(:method) { "post" }
end
it_behaves_like 'has an untitled link' do
let(:link) { 'query' }
let(:href) { api_v3_paths.query(query.id) }
let(:method) { "get" }
end
it_behaves_like 'has an untitled link' do
let(:link) { 'icalUrl' }
let(:href) { mocked_ical_url }
let(:method) { "get" }
end
end
it 'has _type QueryICalUrl' do
expect(subject)
.to be_json_eql('QueryICalUrl'.to_json)
.at_path('_type')
end
end
end
@@ -547,6 +547,26 @@ describe API::V3::Queries::QueryRepresenter do
end
end
describe 'ical url' do
context 'when allowed to subscribe to ical' do
let(:permissions) { %i(share_via_ical) }
it_behaves_like 'has an untitled link' do
let(:link) { 'icalUrl' }
let(:href) { api_v3_paths.query_ical_url(query.id) }
end
end
context 'when lacking permission' do
let(:permissions) { [] }
it 'has no icalUrl link' do
expect(subject)
.not_to have_json_path('_links/icalUrl')
end
end
end
describe 'properties' do
it_behaves_like 'property', :_type do
let(:value) { 'Query' }
+17 -12
View File
@@ -29,9 +29,9 @@
require 'spec_helper'
describe Token::ICal do
let(:user) { build(:user) }
let(:project) { build(:project) }
let(:query) { build(:query, project:) }
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:query) { create(:query, project:) }
let(:name) { 'unique_name' }
it 'inherits from Token::HashedToken' do
@@ -54,7 +54,7 @@ describe Token::ICal do
ical_token_query_assignment_attributes: { query: query, user_id: user.id }
)
expect(ical_token2.errors["ical_token_query_assignment.name"].first).to eq("can't be blank.")
expect(ical_token2.errors["ical_token_query_assignment.name"].first).to eq("is mandatory. Please select a name.")
expect(described_class.where(user_id: user.id)).to be_empty
# if a query and name is given, the token can be created
@@ -115,7 +115,7 @@ describe Token::ICal do
)
expect(ical_token2.errors["ical_token_query_assignment.name"].first).to eq(
"has already been taken for this query and user"
"is already in use. Please select another name."
)
expect(described_class.where(user_id: user.id)).to contain_exactly(
@@ -138,6 +138,8 @@ describe Token::ICal do
describe '#create_and_return_value method' do
it 'expects the query and token name and returns a token value' do
ical_token1_value = nil
expect do
ical_token1_value = described_class.create_and_return_value(
user,
@@ -156,6 +158,9 @@ describe Token::ICal do
end
it 'does not return a token value if token was not successfully persisted' do
ical_token1_value = nil
ical_token2_value = nil
expect do
ical_token1_value = described_class.create_and_return_value(
user,
@@ -163,12 +168,13 @@ describe Token::ICal do
name
)
# same name cannot be used twice for the same query and user
# -> token will not be persisted and no value should be returned
ical_token2_value = described_class.create_and_return_value(
user,
query,
name
)
expect {
ical_token2_value = described_class.create_and_return_value(
user,
query,
name
)
}.to raise_error(ActiveRecord::RecordInvalid)
end.to change { described_class.where(user_id: user.id).count }.by(1)
expect(ical_token1_value).to be_present
@@ -176,7 +182,6 @@ describe Token::ICal do
ical_token1 = described_class.where(user_id: user.id).last
expect(described_class.find_by_plaintext_value(ical_token1_value)).to eq ical_token1
expect(described_class.find_by_plaintext_value(ical_token2_value)).to be_nil
end
end
end
+11
View File
@@ -394,6 +394,17 @@ describe QueryPolicy, type: :controller do
expect(subject.allowed?(query, :share_via_ical)).to be_falsy
end
it 'is true if the user has permission in the project' do
allow(user).to receive(:allowed_to?).with(
:share_calendars,
project,
global: project.nil?
)
.and_return true
expect(subject.allowed?(query, :share_via_ical)).to be_truthy
end
end
end
@@ -0,0 +1,133 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# 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-2013 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 COPYRIGHT and LICENSE files for more details.
#++
require 'spec_helper'
require 'rack/test'
describe 'API v3 Query ICal Url' do
include Rack::Test::Methods
include API::V3::Utilities::PathHelper
describe '#post queries/:id/ical_url' do
let(:project) { create(:project) }
let(:role) { create(:role, permissions:) }
# TODO: check OpenProject::Configuration.ical_subscriptions_enabled configuration
# TODO: :view_work_packages permission is mandatory, otherwise a 404 is returned. Why?
let(:permissions) { [:view_work_packages, :share_calendars] }
let(:user) do
create(:user,
member_in_project: project,
member_through_role: role)
end
let(:query) { create(:query, project:, user:) }
let(:path) { api_v3_paths.query_ical_url(query.id) }
let(:params) { { token_name: "foo" } }
before do
allow(User)
.to receive(:current)
.and_return(user)
header "Content-Type", "application/json"
post path, params.to_json
end
shared_examples_for 'success' do
it 'succeeds' do
expect(last_response.status)
.to eq(201)
end
it 'returns the path pointing to self' do
expect(last_response.body)
.to be_json_eql(path.to_json)
.at_path('_links/self/href')
end
it 'returns the path pointing to the associated query' do
expect(last_response.body)
.to be_json_eql(api_v3_paths.query(query.id).to_json)
.at_path('_links/query/href')
end
it 'returns the tokenized, absolute url pointing to iCalendar endpoint' do
json = JSON.parse(last_response.body)
expect(json['_links']['icalUrl']['href']).to include('http')
expect(json['_links']['icalUrl']['href']).to include(
"projects/#{project.id}/calendars/#{query.id}/ical?ical_token="
)
end
end
context 'when user has sufficient permissions and owns the query' do
it_behaves_like 'success'
end
context 'when user has sufficient permissions and tries to get the iCalendar url of the public query of another user' do
let(:role_of_other_user) { create(:role, permissions: [:view_work_packages]) }
let(:other_user) do
create(:user,
member_in_project: project,
member_through_role: role_of_other_user)
end
let(:query) { create(:query, project:, user: other_user, public: true) }
let(:path) { api_v3_paths.query_ical_url(query.id) }
it_behaves_like 'success'
end
context 'when user has no access to the associated project' do
let(:other_project) { create(:project) }
let(:query) { create(:query, project: other_project, user:) }
let(:path) { api_v3_paths.query_ical_url(query.id) }
it_behaves_like 'not found'
end
context 'when user tries to get an iCalendar url from a private query of another user' do
let(:other_user) { create(:user) }
let(:query) { create(:query, project:, user: other_user, public: false) }
let(:path) { api_v3_paths.query_ical_url(query.id) }
it_behaves_like 'not found'
end
context 'when user has insufficient permissions' do
# TODO: :view_work_packages permission is mandatory, otherwise a 404 is returned. Why?
let(:permissions) { [:view_work_packages] } # share_calendars is missing
it_behaves_like 'unauthorized access'
end
context 'when query does not exist' do
let(:path) { api_v3_paths.query_ical_url(query.id+42) }
it_behaves_like 'not found'
end
end
end
+2 -2
View File
@@ -66,7 +66,7 @@ describe 'my routes' do
action: 'deletion_info')
end
it '/my/revoke_all_ical_tokens_of_query DELETE routes to my#revoke_all_ical_tokens_of_query' do
expect(delete('/my/revoke_all_ical_tokens_of_query')).to route_to('my#revoke_all_ical_tokens_of_query')
it '/my/revoke_ical_token DELETE routes to my#revoke_ical_token' do
expect(delete('/my/revoke_ical_token')).to route_to('my#revoke_ical_token')
end
end