Merge branch 'dev' into implementation/42160-add-download-file-direct-action-to-hover-menu

pull/10834/head
Eric Schubert 2 years ago
commit 675975ac9f
No known key found for this signature in database
GPG Key ID: 1D346C019BD4BAA2
  1. 6
      .github/workflows/brakeman-scan-core.yml
  2. 5
      .github/workflows/continuous-delivery.yml
  3. 3
      .github/workflows/test-core.yml
  4. 256
      app/services/users/replace_mentions_service.rb
  5. 17
      app/workers/principals/delete_job.rb
  6. 4
      config/initializers/secure_headers.rb
  7. 2
      frontend/src/app/shared/components/datepicker/banner/datepicker-banner.component.html
  8. 8
      frontend/src/app/shared/components/datepicker/datepicker.modal.service.ts
  9. 45
      frontend/src/app/shared/components/datepicker/datepicker.modal.ts
  10. 22
      frontend/src/global_styles/content/_datepicker.sass
  11. 1
      spec/features/work_packages/table/scheduling/manual_scheduling_spec.rb
  12. 436
      spec/services/users/replace_mentions_service_integration_spec.rb
  13. 6
      spec/support/edit_fields/date_edit_field.rb
  14. 43
      spec/workers/principals/delete_job_integration_spec.rb

@ -10,8 +10,14 @@ on:
schedule:
- cron: '10 6 * * 1'
permissions:
contents: read
jobs:
brakeman-scan:
permissions:
contents: read # for actions/checkout to fetch code
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
if: github.repository == 'opf/openproject'
name: Brakeman Scan
runs-on: ubuntu-latest

@ -4,8 +4,13 @@ on:
branches:
- dev
- release/*
permissions:
contents: read
jobs:
trigger_downstream_workflow:
permissions:
contents: none
if: github.repository == 'opf/openproject'
runs-on: ubuntu-latest
steps:

@ -14,6 +14,9 @@ on:
- 'docs/**'
- 'help/**'
permissions:
contents: read
jobs:
units:
name: Units

@ -0,0 +1,256 @@
# OpenProject is an open source project management software.
# Copyright (C) 2010-2022 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.
# Rewrites mentions in user provided text (e.g. work package journals) from one user to another.
# No data is to be removed.
module Users
class ReplaceMentionsService
include ActiveRecord::Sanitization
def self.replacements
[
{ class: WorkPackage, column: :description },
{ class: Journal::WorkPackageJournal, column: :description },
{ class: Document, column: :description },
{ class: Journal, column: :notes },
{ class: Comment, column: :comments },
{
class: CustomValue,
column: :value,
condition: <<~SQL.squish
EXISTS (
SELECT 1
FROM #{CustomField.table_name}
WHERE field_format = 'text'
AND #{CustomValue.table_name}.custom_field_id = custom_fields.id
)
SQL
},
{
class: Journal::CustomizableJournal,
column: :value,
condition: <<~SQL.squish
EXISTS (
SELECT 1
FROM #{CustomField.table_name}
WHERE field_format = 'text'
AND #{Journal::CustomizableJournal.table_name}.custom_field_id = custom_fields.id
)
SQL
},
{ class: MeetingContent, column: :text },
{ class: Journal::MeetingContentJournal, column: :text },
{ class: Message, column: :content },
{ class: Journal::MessageJournal, column: :content },
{ class: News, column: :description },
{ class: Journal::NewsJournal, column: :description },
{ class: Project, column: :description },
{ class: Projects::Status, column: :explanation },
{ class: WikiContent, column: :text },
{ class: Journal::WikiContentJournal, column: :text }
]
end
def initialize(*classes)
self.replacements = if classes.any?
self.class.replacements.select { |r| classes.include?(r[:class]) }
else
self.class.replacements
end
end
def call(from:, to:)
check_input(from, to)
rewrite(from, to)
ServiceResult.success
end
private
attr_accessor :replacements
def check_input(from, to)
raise ArgumentError unless (from.is_a?(User) || from.is_a?(Group)) && to.is_a?(User)
end
def rewrite(from, to)
# If we have only one replacement configured for this instance of the service, we do it ourselves.
# Otherwise, we instantiate instances of the service's class for every class for which replacements
# are to be carried out.
# That way, instance variables can be used which prevents having method interfaces with 4 or 5 parameters.
if replacements.length == 1
rewrite_column(from, to)
else
replacements.map do |replacement|
self.class.new(replacement[:class]).call(from:, to:)
end
end
end
def rewrite_column(from, to)
sql = <<~SQL.squish
UPDATE #{table_name} sink
SET #{column_name} = source.replacement
FROM
(SELECT
#{table_name}.id,
#{replace_sql(from, to)} replacement
FROM
#{table_name}
WHERE
#{condition_sql(from)}
) source
WHERE
source.id = sink.id
SQL
klass
.connection
.execute sql
end
def replace_sql(from, to)
hash_replace(mention_tag_replace(from,
to),
from,
to)
end
def condition_sql(from)
mention = '<mention_*data-id="%i"_*</mention>'
hash = if from.is_a?(User)
'user#((%i)|("%s")|("%s"))\s'
else
'group#%i\s'
end
hash_values = if from.is_a?(User)
[
sanitize_sql_like(from.mail),
sanitize_sql_like(from.login)
]
else
[]
end
sql = <<~SQL.squish
#{table_name}.#{column_name} SIMILAR TO '_*((#{mention})|(#{hash}))_*'
SQL
if condition
sql += "AND #{condition}"
end
sanitize_sql_for_conditions [sql,
from.id,
from.id] + hash_values
end
def mention_tag_replace(from, to)
regexp_replace(
"#{table_name}.#{column_name}",
'<mention.+data-id="%i".+</mention>',
'<mention class="mention" data-id="%i" data-type="user" data-text="@%s">@%s</mention>',
[from.id,
to.id,
sanitize_sql_like(to.name),
sanitize_sql_like(to.name)]
)
end
def hash_replace(source, from, to)
search = if from.is_a?(User)
'user#((%i)|("%s")|("%s"))\s'
else
'group#%i\s'
end
values = if from.is_a?(User)
[
from.id,
sanitize_sql_like(from.login),
sanitize_sql_like(from.mail),
to.id
]
else
[
from.id,
to.id
]
end
regexp_replace(
source,
search,
'user#%i ',
values
)
end
def regexp_replace(source, search, replacement, values)
sql = <<~SQL.squish
REGEXP_REPLACE(
#{source},
'#{search}',
'#{replacement}',
'g'
)
SQL
sanitize_sql_for_conditions [sql].concat(values)
end
def sanitize_sql_like(string)
klass.sanitize_sql_like(string)
end
def sanitize_sql_for_conditions(string)
klass.sanitize_sql_for_conditions string
end
def journal_classes
[Journal] + Journal::BaseJournal.subclasses
end
def klass
replacements[0][:class]
end
def table_name
replacements[0][:class].table_name
end
def column_name
replacements[0][:column]
end
def condition
replacements[0][:condition]
end
end
end

@ -33,6 +33,7 @@ class Principals::DeleteJob < ApplicationJob
Principal.transaction do
delete_associated(principal)
replace_references(principal)
replace_mentions(principal)
update_cost_queries(principal)
remove_members(principal)
@ -46,9 +47,19 @@ class Principals::DeleteJob < ApplicationJob
Principals::ReplaceReferencesService
.new
.call(from: principal, to: DeletedUser.first)
.tap do |call|
raise ActiveRecord::Rollback if call.failure?
end
.on_failure { raise ActiveRecord::Rollback }
end
def replace_mentions(principal)
# Breaking abstraction here.
# Doing the replacement is a very costly operation while at the same time,
# placeholder users can't be mentioned.
return unless principal.is_a?(User) || principal.is_a?(Group)
Users::ReplaceMentionsService
.new
.call(from: principal, to: DeletedUser.first)
.on_failure { raise ActiveRecord::Rollback }
end
def delete_associated(principal)

@ -38,7 +38,9 @@ Rails.application.config.after_initialize do
# Add proxy configuration for Angular CLI to csp
if FrontendAssetHelper.assets_proxied?
proxied = ['ws://localhost:*', 'http://localhost:*', FrontendAssetHelper.cli_proxy]
proxied = ['ws://localhost:3000', 'http://localhost:3000',
'ws://localhost:4200', 'http://localhost:4200',
FrontendAssetHelper.cli_proxy]
connect_src += proxied
assets_src += proxied
end

@ -20,7 +20,7 @@
</op-modal-banner>
<ng-container *ngIf="!scheduleManually && !isParent">
<op-modal-banner
*ngIf="(hasPrecedingRelations$ | async) === true && (hasFollowingRelations$ | async) === false"
*ngIf="(hasPrecedingRelations$ | async) === true && (hasFollowingRelations$ | async) === false && !isChild"
type="info"
[title]="text.start_date_limited_by_relations"
[subtitle]="text.click_on_show_relations_to_open_gantt"

@ -55,12 +55,16 @@ export class DatepickerModalService {
private changeset:WorkPackageChangeset = this.locals.changeset as WorkPackageChangeset;
precedingWorkPackages$:Observable<{ id:string }[]> = this
precedingWorkPackages$:Observable<{ id:string, dueDate?:string, date?:string }[]> = this
.apiV3Service
.work_packages
.signalled(
ApiV3Filter('precedes', '=', [this.changeset.id]),
['elements/id'],
[
'elements/id',
'elements/dueDate',
'elements/date',
],
)
.pipe(
map((collection:IHALCollection<{ id:string }>) => collection._embedded.elements || []),

@ -55,6 +55,8 @@ import {
} from 'flatpickr/dist/types/instance';
import flatpickr from 'flatpickr';
import { DatepickerModalService } from 'core-app/shared/components/datepicker/datepicker.modal.service';
import { take } from 'rxjs/operators';
import { activeFieldContainerClassName } from 'core-app/shared/components/fields/edit/edit-form/edit-form';
export type DateKeys = 'date'|'start'|'end';
@ -134,9 +136,15 @@ export class DatePickerModalComponent extends OpModalComponent implements AfterV
}
ngAfterViewInit():void {
this.initializeDatepicker();
this.onDataChange();
this
.datepickerService
.precedingWorkPackages$
.pipe(
take(1),
).subscribe((relation) => {
this.initializeDatepicker(this.minimalDateFromPrecedingRelationship(relation));
this.onDataChange();
});
}
changeSchedulingMode():void {
@ -225,7 +233,7 @@ export class DatePickerModalComponent extends OpModalComponent implements AfterV
return !this.scheduleManually && !!this.changeset.value('scheduleManually');
}
private initializeDatepicker() {
private initializeDatepicker(minimalDate?:Date|null) {
this.datePickerInstance?.destroy();
this.datePickerInstance = new DatePicker(
this.injector,
@ -237,6 +245,7 @@ export class DatePickerModalComponent extends OpModalComponent implements AfterV
inline: true,
onReady: (selectedDates, dateStr, instance) => {
this.toggleDisabledState(instance);
this.reposition(jQuery(this.modalContainer.nativeElement), jQuery(`.${activeFieldContainerClassName}`));
},
onChange: (dates:Date[]) => {
this.handleDatePickerChange(dates);
@ -248,6 +257,10 @@ export class DatePickerModalComponent extends OpModalComponent implements AfterV
dayElem.classList.add('flatpickr-non-working-day');
}
if (minimalDate && dayElem.dateObj <= minimalDate) {
dayElem.classList.add('flatpickr-disabled');
}
dayElem.setAttribute('data-iso-date', dayElem.dateObj.toISOString());
},
},
@ -349,4 +362,28 @@ export class DatePickerModalComponent extends OpModalComponent implements AfterV
instance.calendarContainer.classList.add('disabled');
}
}
private minimalDateFromPrecedingRelationship(relations:{ id:string, dueDate?:string, date?:string }[]):Date|null {
if (relations.length === 0) {
return null;
}
let minimalDate:Date|null = null;
relations.forEach((relation) => {
if (!relation.dueDate && !relation.date) {
return;
}
const relationDate = relation.dueDate || relation.date;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const parsedRelationDate = this.datepickerService.parseDate(relationDate!);
if (!minimalDate || minimalDate < parsedRelationDate) {
minimalDate = parsedRelationDate === '' ? null : parsedRelationDate;
}
});
return minimalDate;
}
}

@ -28,6 +28,17 @@
$datepicker--border-radius: 5px
@mixin disabled-day
background: $spot-color-basic-white
color: $spot-color-basic-gray-4 !important
border-radius: 0
background: $spot-color-basic-white
pointer-events: none
cursor: not-allowed
&:hover
border-color: transparent
.flatpickr-current-month
display: flex !important
@ -37,7 +48,8 @@ $datepicker--border-radius: 5px
.flatpickr-day
&.flatpickr-disabled,
&.flatpickr-disabled
@include disabled-day
&.flatpickr-non-working-day
background: $spot-color-basic-gray-6
border-radius: 0
@ -132,13 +144,7 @@ $datepicker--border-radius: 5px
&.disabled
.flatpickr-days
.flatpickr-day
color: $spot-color-basic-gray-4
background: $spot-color-basic-white
pointer-events: none
cursor: not-allowed
&:hover
border-color: transparent
@include disabled-day
&.selected,
&.startRange,

@ -98,6 +98,7 @@ describe 'Manual scheduling', js: true do
start_date.toggle_scheduling_mode
start_date.expect_scheduling_mode manually: true
start_date.expect_calendar
# Expect not editable
start_date.within_modal do

@ -0,0 +1,436 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 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 Users::ReplaceMentionsService, 'integration' do
subject(:service_call) { instance.call(from: principal, to: to_user) }
shared_let(:other_user) { create(:user, firstname: 'Frank', lastname: 'Herbert') }
shared_let(:user) { create(:user, firstname: 'Isaac', lastname: 'Asimov') }
shared_let(:group) { create(:group, lastname: 'Sci-Fi') }
shared_let(:to_user) { create :user, firstname: 'Philip K.', lastname: 'Dick' }
let(:instance) do
described_class.new
end
let(:principal) { user }
it 'is successful' do
expect(service_call)
.to be_success
end
shared_examples_for 'text replacement' do |attribute|
before do
service_call
model.reload
end
it "replaces #{attribute}" do
expect(model.send(attribute))
.to be_html_eql expected_text
end
end
shared_examples_for 'rewritten mention' do |factory, attribute|
let(:additional_properties) { {} }
let!(:model) do
create(factory, attribute => text, **additional_properties)
end
context 'with the replaced user in mention tags' do
let(:principal) { user }
it_behaves_like 'text replacement', attribute do
let(:text) do
<<~TEXT
<mention class="mention"
data-id="#{user.id}"
data-type="user"
data-text="@#{user.name}">
@#{user.name}
</mention>
TEXT
end
let(:expected_text) do
<<~TEXT.squish
<mention class="mention"
data-id="#{to_user.id}"
data-type="user"
data-text="@#{to_user.name}">@#{to_user.name}</mention>
TEXT
end
end
end
context 'with a different user in mention tags' do
let(:principal) { user }
it_behaves_like 'text replacement', attribute do
let(:text) do
<<~TEXT
<mention class="mention"
data-id="#{other_user.id}"
data-type="user"
data-text="@#{other_user.name}">
@#{other_user.name}
</mention>
TEXT
end
let(:expected_text) do
<<~TEXT.squish
<mention class="mention"
data-id="#{other_user.id}"
data-type="user"
data-text="@#{other_user.name}">@#{other_user.name}</mention>
TEXT
end
end
end
context 'with the replaced user in a user#ID notation' do
let(:principal) { user }
it_behaves_like 'text replacement', attribute do
let(:text) do
<<~TEXT
Lorem ipsum
user##{user.id} Lorem ipsum
Lorem ipsum
TEXT
end
let(:expected_text) do
<<~TEXT
Lorem ipsum
user##{to_user.id} Lorem ipsum
Lorem ipsum
TEXT
end
end
end
context 'with a different user in a user#ID notation' do
let(:principal) { user }
it_behaves_like 'text replacement', attribute do
let(:text) do
<<~TEXT
Lorem ipsum
user##{other_user.id} Lorem ipsum
Lorem ipsum
TEXT
end
let(:expected_text) do
<<~TEXT
Lorem ipsum
user##{other_user.id} Lorem ipsum
Lorem ipsum
TEXT
end
end
end
context 'with the replaced user in a user#"LOGIN" notation' do
let(:principal) { user }
it_behaves_like 'text replacement', attribute do
let(:text) do
<<~TEXT
Lorem ipsum
user#"#{user.login}" Lorem ipsum
Lorem ipsum
TEXT
end
let(:expected_text) do
<<~TEXT
Lorem ipsum
user##{to_user.id} Lorem ipsum
Lorem ipsum
TEXT
end
end
end
context 'with a different user in a user#"LOGIN" notation' do
let(:principal) { user }
it_behaves_like 'text replacement', attribute do
let(:text) do
<<~TEXT
Lorem ipsum
user#"#{other_user.login}" Lorem ipsum
Lorem ipsum
TEXT
end
let(:expected_text) do
<<~TEXT
Lorem ipsum
user#"#{other_user.login}" Lorem ipsum
Lorem ipsum
TEXT
end
end
end
context 'with the replaced user in a user#"MAIL" notation' do
let(:principal) { user }
it_behaves_like 'text replacement', attribute do
let(:text) do
<<~TEXT
Lorem ipsum
user#"#{user.mail}" Lorem ipsum
Lorem ipsum
TEXT
end
let(:expected_text) do
<<~TEXT
Lorem ipsum
user##{to_user.id} Lorem ipsum
Lorem ipsum
TEXT
end
end
end
context 'with a different user in a user#"MAIL" notation' do
let(:principal) { user }
it_behaves_like 'text replacement', attribute do
let(:text) do
<<~TEXT
Lorem ipsum
user#"#{other_user.mail}" Lorem ipsum
Lorem ipsum
TEXT
end
let(:expected_text) do
<<~TEXT
Lorem ipsum
user#"#{other_user.mail}" Lorem ipsum
Lorem ipsum
TEXT
end
end
end
context 'with the replaced group in mention tags' do
let(:principal) { group }
it_behaves_like 'text replacement', attribute do
let(:text) do
<<~TEXT
<mention class="mention"
data-id="#{group.id}"
data-type="group"
data-text="@#{group.name}">
@#{group.name}
</mention>
TEXT
end
let(:expected_text) do
<<~TEXT.squish
<mention class="mention"
data-id="#{to_user.id}"
data-type="user"
data-text="@#{to_user.name}">@#{to_user.name}</mention>
TEXT
end
end
end
context 'with the replaced group in a group#ID notation' do
let(:principal) { group }
it_behaves_like 'text replacement', attribute do
let(:text) do
<<~TEXT
Lorem ipsum
group##{group.id} Lorem ipsum
Lorem ipsum
TEXT
end
let(:expected_text) do
<<~TEXT
Lorem ipsum
user##{to_user.id} Lorem ipsum
Lorem ipsum
TEXT
end
end
end
end
context 'for work package description' do
it_behaves_like 'rewritten mention', :work_package, :description
end
context 'for work package description with dangerous mails' do
let(:dangerous_user) do
build(:user,
firstname: 'Dangerous',
lastname: 'User',
mail: "'); DELETE FROM work_packages; SELECT ('").tap do |user|
user.save(validate: false)
end
end
let(:principal) { dangerous_user }
it 'escapes the malicious input' do
expect { service_call }
.not_to raise_error
end
end
context 'for work package journal description' do
it_behaves_like 'rewritten mention', :journal_work_package_journal, :description
end
context 'for journal notes' do
it_behaves_like 'rewritten mention', :journal, :notes
end
context 'for comment comments' do
it_behaves_like 'rewritten mention', :comment, :comments
end
context 'for custom_value value' do
it_behaves_like 'rewritten mention', :custom_value, :value do
let(:additional_properties) { { custom_field: create(:text_wp_custom_field) } }
end
end
context 'for customizable_journal value' do
it_behaves_like 'rewritten mention', :journal_customizable_journal, :value do
let(:additional_properties) { { journal: create(:journal), custom_field: create(:text_wp_custom_field) } }
end
end
context 'for documents description' do
it_behaves_like 'rewritten mention', :document, :description
end
context 'for meeting_contents text' do
it_behaves_like 'rewritten mention', :meeting_agenda, :text
end
context 'for meeting_content_journals text' do
it_behaves_like 'rewritten mention', :journal_meeting_content_journal, :text
end
context 'for messages content' do
it_behaves_like 'rewritten mention', :message, :content
end
context 'for message_journals content' do
it_behaves_like 'rewritten mention', :journal_message_journal, :content do
let(:additional_properties) { { forum_id: 1 } }
end
end
context 'for news description' do
it_behaves_like 'rewritten mention', :news, :description
end
context 'for news_journals description' do
it_behaves_like 'rewritten mention', :journal_news_journal, :description
end
context 'for project description' do
it_behaves_like 'rewritten mention', :project, :description
end
context 'for project_status explanation' do
it_behaves_like 'rewritten mention', :project_status, :explanation
end
context 'for wiki_content text' do
it_behaves_like 'rewritten mention', :wiki_content, :text
end
context 'for wiki_content_journals text' do
it_behaves_like 'rewritten mention', :journal_wiki_content_journal, :text
end
context 'for a group for to' do
subject(:service_call) { instance.call(from: user, to: create(:group)) }
it 'raises an error' do
expect { service_call }
.to raise_error ArgumentError
end
end
context 'for a placeholder user for from' do
subject(:service_call) { instance.call(from: create(:placeholder_user), to: to_user) }
it 'raises an error' do
expect { service_call }
.to raise_error ArgumentError
end
end
context 'for a placeholder user for to' do
subject(:service_call) { instance.call(from: user, to: create(:placeholder_user)) }
it 'raises an error' do
expect { service_call }
.to raise_error ArgumentError
end
end
end

@ -89,6 +89,12 @@ class DateEditField < EditField
expect(page).to have_no_selector("#{modal_selector} #{input_selector}")
end
def expect_calendar
within_modal do
expect(page).to have_selector(".flatpickr-calendar")
end
end
def update(value, save: true, expect_failure: false)
# Retry to set attributes due to reloading the page after setting
# an attribute, which may cause an input not to open properly.

@ -96,7 +96,7 @@ describe Principals::DeleteJob, type: :model do
job
end
it { expect(LaborBudgetItem.find_by_id(item.id)).to eq(item) }
it { expect(LaborBudgetItem.find_by(id: item.id)).to eq(item) }
it { expect(item.user_id).to eq(principal.id) }
end
@ -107,7 +107,7 @@ describe Principals::DeleteJob, type: :model do
user: principal,
project: work_package.project,
units: 100.0,
spent_on: Date.today,
spent_on: Time.zone.today,
work_package:,
comments: '')
end
@ -159,7 +159,7 @@ describe Principals::DeleteJob, type: :model do
job
end
it { expect(HourlyRate.find_by_id(hourly_rate.id)).to eq(hourly_rate) }
it { expect(HourlyRate.find_by(id: hourly_rate.id)).to eq(hourly_rate) }
it { expect(hourly_rate.reload.user_id).to eq(principal.id) }
end
@ -257,7 +257,7 @@ describe Principals::DeleteJob, type: :model do
it 'removes the query' do
job
expect(CostQuery.find_by_id(query.id)).to be_nil
expect(CostQuery.find_by(id: query.id)).to be_nil
end
end
@ -271,7 +271,7 @@ describe Principals::DeleteJob, type: :model do
end
it 'leaves the query' do
expect(CostQuery.find_by_id(query.id)).to eq(query)
expect(CostQuery.find_by(id: query.id)).to eq(query)
end
it 'rewrites the user reference' do
@ -366,6 +366,37 @@ describe Principals::DeleteJob, type: :model do
end
end
shared_examples_for 'mention rewriting' do
let(:text) do
<<~TEXT
<mention class="mention"
data-id="#{principal.id}"
data-type="user"
data-text="@#{principal.name}">
@#{principal.name}
</mention>
TEXT
end
let(:expected_text) do
<<~TEXT.squish
<mention class="mention"
data-id="#{deleted_user.id}"
data-type="user"
data-text="@#{deleted_user.name}">@#{deleted_user.name}</mention>
TEXT
end
let!(:work_package) { create(:work_package, description: text) }
before do
job
end
it 'rewrites the mentioning in the text' do
expect(work_package.reload.description)
.to include expected_text
end
end
context 'with a user' do
it_behaves_like 'removes the principal'
it_behaves_like 'work_package handling'
@ -381,6 +412,7 @@ describe Principals::DeleteJob, type: :model do
it_behaves_like 'private cost_query handling'
it_behaves_like 'public cost_query handling'
it_behaves_like 'cost_query handling'
it_behaves_like 'mention rewriting'
end
context 'with a group' do
@ -390,6 +422,7 @@ describe Principals::DeleteJob, type: :model do
it_behaves_like 'removes the principal'
it_behaves_like 'work_package handling'
it_behaves_like 'member handling'
it_behaves_like 'mention rewriting'
context 'with user only in project through group' do
let(:user) do

Loading…
Cancel
Save