From 0caba9722cd0b18789cec427bcb0a521f6a7760b Mon Sep 17 00:00:00 2001 From: Jan Sandbrink Date: Wed, 27 May 2026 13:41:56 +0200 Subject: [PATCH 1/2] Add query to search for wiki pages This can be used to discover wiki pages that exist in the provider and could be linked to. --- .../adapters/input/search_pages_contract.rb | 41 +++++++ .../wikis/adapters/input/search_pages.rb | 39 +++++++ .../providers/internal/queries/page_info.rb | 44 ++++---- .../internal/queries/search_pages.rb | 52 +++++++++ .../adapters/providers/internal/registry.rb | 1 + .../providers/xwiki/queries/search_pages.rb | 53 +++++++++ .../adapters/providers/xwiki/registry.rb | 1 + .../internal/queries/search_pages_spec.rb | 103 ++++++++++++++++++ 8 files changed, 314 insertions(+), 20 deletions(-) create mode 100644 modules/wikis/app/contracts/wikis/adapters/input/search_pages_contract.rb create mode 100644 modules/wikis/app/models/wikis/adapters/input/search_pages.rb create mode 100644 modules/wikis/app/services/wikis/adapters/providers/internal/queries/search_pages.rb create mode 100644 modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/search_pages.rb create mode 100644 modules/wikis/spec/services/wikis/adapters/providers/internal/queries/search_pages_spec.rb diff --git a/modules/wikis/app/contracts/wikis/adapters/input/search_pages_contract.rb b/modules/wikis/app/contracts/wikis/adapters/input/search_pages_contract.rb new file mode 100644 index 00000000000..c69b82068f0 --- /dev/null +++ b/modules/wikis/app/contracts/wikis/adapters/input/search_pages_contract.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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. +#++ + +module Wikis + module Adapters + module Input + class SearchPagesContract < DryApplicationContract + params do + required(:query).filled(:string) + end + end + end + end +end diff --git a/modules/wikis/app/models/wikis/adapters/input/search_pages.rb b/modules/wikis/app/models/wikis/adapters/input/search_pages.rb new file mode 100644 index 00000000000..d26149924c5 --- /dev/null +++ b/modules/wikis/app/models/wikis/adapters/input/search_pages.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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. +#++ + +module Wikis::Adapters::Input + SearchPages = Data.define(:query) do + private_class_method :new + + def self.build(query:, contract: SearchPagesContract.new) + contract.call(query:).to_monad.fmap { new(**it.to_h) } + end + end +end diff --git a/modules/wikis/app/services/wikis/adapters/providers/internal/queries/page_info.rb b/modules/wikis/app/services/wikis/adapters/providers/internal/queries/page_info.rb index 0cdf09a41df..7a80bb0ccd3 100644 --- a/modules/wikis/app/services/wikis/adapters/providers/internal/queries/page_info.rb +++ b/modules/wikis/app/services/wikis/adapters/providers/internal/queries/page_info.rb @@ -34,33 +34,37 @@ module Wikis module Internal module Queries class PageInfo < BaseQuery + class << self + def wiki_page_to_page_info(wiki_page, provider:) + Results::PageInfo.new( + identifier: wiki_page.id.to_s, + title: wiki_page.title, + provider:, + href: url_for(only_path: true, + controller: "/wiki", + action: "show", + project_id: wiki_page.project.identifier, + id: wiki_page.slug) + ) + end + + private + + delegate :url_for, to: :url_helpers + + def url_helpers + @url_helpers ||= OpenProject::StaticRouting::StaticRouter.new.url_helpers + end + end + def call(input_data:, auth_strategy:) Adapters::Authentication[auth_strategy].call do |user| wiki_page = WikiPage.visible(user).find_by(id: input_data.identifier) return failure(code: :not_found) if wiki_page.nil? - success( - Results::PageInfo.new( - identifier: input_data.identifier, - title: wiki_page.title, - provider:, - href: url_for(only_path: true, - controller: "/wiki", - action: "show", - project_id: wiki_page.project.identifier, - id: wiki_page.slug) - ) - ) + success(self.class.wiki_page_to_page_info(wiki_page, provider:)) end end - - private - - delegate :url_for, to: :url_helpers - - def url_helpers - OpenProject::StaticRouting::StaticRouter.new.url_helpers - end end end end diff --git a/modules/wikis/app/services/wikis/adapters/providers/internal/queries/search_pages.rb b/modules/wikis/app/services/wikis/adapters/providers/internal/queries/search_pages.rb new file mode 100644 index 00000000000..819c6453bb8 --- /dev/null +++ b/modules/wikis/app/services/wikis/adapters/providers/internal/queries/search_pages.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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. +#++ + +module Wikis + module Adapters + module Providers + module Internal + module Queries + class SearchPages < BaseQuery + MAXIMUM_RESULTS = 50 + + def call(input_data:, auth_strategy:) + success( + WikiPage.visible(auth_strategy.user) + .where("title ILIKE ?", "%#{input_data.query}%") + .limit(MAXIMUM_RESULTS) + .map { PageInfo.wiki_page_to_page_info(it, provider:) } + ) + end + end + end + end + end + end +end diff --git a/modules/wikis/app/services/wikis/adapters/providers/internal/registry.rb b/modules/wikis/app/services/wikis/adapters/providers/internal/registry.rb index 9e747ca7994..87b21c789b5 100644 --- a/modules/wikis/app/services/wikis/adapters/providers/internal/registry.rb +++ b/modules/wikis/app/services/wikis/adapters/providers/internal/registry.rb @@ -53,6 +53,7 @@ module Wikis register(:page_info, Queries::PageInfo) register(:referencing_pages, Queries::ReferencingPages) register(:relation_page_links, Queries::RelationPageLinks) + register(:search_pages, Queries::SearchPages) end end end diff --git a/modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/search_pages.rb b/modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/search_pages.rb new file mode 100644 index 00000000000..bd02594e1f6 --- /dev/null +++ b/modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/search_pages.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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. +#++ + +module Wikis + module Adapters + module Providers + module XWiki + module Queries + class SearchPages < BaseQuery + def call(input_data:, **) + # TODO: use real API endpoints once available + + titles = [ + "#{input_data.query} makes XWiki special", + "API documentation of #{input_data.query}", + "A brief introduction on configuring your own #{input_data.query}." + ] + + success(titles.map { Results::PageInfo.new(identifier: "1338", title: it, href: "#", provider:) }) + end + end + end + end + end + end +end diff --git a/modules/wikis/app/services/wikis/adapters/providers/xwiki/registry.rb b/modules/wikis/app/services/wikis/adapters/providers/xwiki/registry.rb index c3a53e4f52d..534328ecf15 100644 --- a/modules/wikis/app/services/wikis/adapters/providers/xwiki/registry.rb +++ b/modules/wikis/app/services/wikis/adapters/providers/xwiki/registry.rb @@ -64,6 +64,7 @@ module Wikis register(:page_info, Queries::PageInfo) register(:referencing_pages, Queries::ReferencingPages) register(:relation_page_links, Queries::RelationPageLinks) + register(:search_pages, Queries::SearchPages) end namespace("validators") do diff --git a/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/search_pages_spec.rb b/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/search_pages_spec.rb new file mode 100644 index 00000000000..4cf616a7ee4 --- /dev/null +++ b/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/search_pages_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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" + +RSpec.describe Wikis::Adapters::Providers::Internal::Queries::SearchPages do + subject { described_class.new(model: provider).call(input_data:, auth_strategy:) } + + let(:provider) { create(:internal_wiki_provider) } + let(:input_data) { Wikis::Adapters::Input::SearchPages.build(query:).value! } + let(:auth_strategy) { provider.auth_strategy_for(current_user).value! } + let(:query) { wiki_page.title } + + let(:wiki_page) { create(:wiki_page, title: "Wiki Page with a Title you will love") } + let(:wiki_project) { wiki_page.project } + let(:wiki_project_permissions) { %i[view_wiki_pages] } + + current_user { create(:user) } + + before do + create(:member, project: wiki_project, + user: current_user, + roles: [create(:project_role, permissions: wiki_project_permissions)]) + + wiki_page + end + + it { is_expected.to be_success } + + it "returns pages matching the search term exactly" do + expect(subject.value!).not_to be_empty + expect(subject.value!.first.title).to eq(wiki_page.title) + end + + context "when the search term only matches partially" do + let(:query) { "a Title" } + + it { is_expected.to be_success } + + it "returns matching pages" do + expect(subject.value!).not_to be_empty + expect(subject.value!.first.title).to eq(wiki_page.title) + end + end + + context "when the search term has wrong casing" do + let(:query) { wiki_page.title.downcase } + + it { is_expected.to be_success } + + it "returns matching pages" do + expect(subject.value!).not_to be_empty + expect(subject.value!.first.title).to eq(wiki_page.title) + end + end + + context "when there are no matching pages" do + let(:query) { "the title" } + + it { is_expected.to be_success } + + it "returns an empty result" do + expect(subject.value!).to eq([]) + end + end + + context "when user can't see a matching wiki page" do + let(:wiki_project_permissions) { %i[] } + + it { is_expected.to be_success } + + it "returns an empty result" do + expect(subject.value!).to eq([]) + end + end +end From 0b66ff3fe0e9178367b8f9e188800dcffb1b11ff Mon Sep 17 00:00:00 2001 From: Jan Sandbrink Date: Thu, 28 May 2026 17:41:19 +0200 Subject: [PATCH 2/2] Ensure current_user is not set in query specs Queries are supposed to return results for the user passed in the auth strategy. If the current user is set, it's easy for the spec to accidentially succeed, because the query implementation used the global User.current instead. --- .../providers/internal/queries/page_info_query_spec.rb | 6 +++--- .../internal/queries/referencing_pages_query_spec.rb | 6 +++--- .../internal/queries/relation_page_links_query_spec.rb | 6 +++--- .../providers/internal/queries/search_pages_spec.rb | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/page_info_query_spec.rb b/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/page_info_query_spec.rb index 3ca4f91fb8e..672bfd211e6 100644 --- a/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/page_info_query_spec.rb +++ b/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/page_info_query_spec.rb @@ -35,7 +35,7 @@ RSpec.describe Wikis::Adapters::Providers::Internal::Queries::PageInfo do let(:provider) { create(:internal_wiki_provider) } let(:input_data) { Wikis::Adapters::Input::PageInfo.build(identifier:).value! } - let(:auth_strategy) { provider.auth_strategy_for(current_user).value! } + let(:auth_strategy) { provider.auth_strategy_for(user).value! } let(:identifier) { wiki_page.id.to_s } let(:wiki_page) { create(:wiki_page) } @@ -43,10 +43,10 @@ RSpec.describe Wikis::Adapters::Providers::Internal::Queries::PageInfo do let(:other_wiki_page) { create(:wiki_page) } let(:permissions) { %i[view_work_packages view_wiki_pages] } - current_user { create(:user) } + let(:user) { create(:user) } before do - create(:member, project:, user: current_user, roles: [create(:project_role, permissions:)]) + create(:member, project:, user:, roles: [create(:project_role, permissions:)]) end it { is_expected.to be_success } diff --git a/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/referencing_pages_query_spec.rb b/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/referencing_pages_query_spec.rb index 177b342005c..f8838b066d9 100644 --- a/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/referencing_pages_query_spec.rb +++ b/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/referencing_pages_query_spec.rb @@ -35,7 +35,7 @@ RSpec.describe Wikis::Adapters::Providers::Internal::Queries::ReferencingPages d let(:provider) { create(:internal_wiki_provider) } let(:input_data) { Wikis::Adapters::Input::ReferencingPages.build(linkable:).value! } - let(:auth_strategy) { provider.auth_strategy_for(current_user).value! } + let(:auth_strategy) { provider.auth_strategy_for(user).value! } let(:linkable) { create(:work_package) } let(:wiki_page) { create(:wiki_page) } @@ -48,11 +48,11 @@ RSpec.describe Wikis::Adapters::Providers::Internal::Queries::ReferencingPages d ] end - current_user { create(:user) } + let(:user) { create(:user) } before do create(:member, project: wiki_project, - user: current_user, + user:, roles: [create(:project_role, permissions: wiki_project_permissions)]) reverse_page_links.each(&:save!) diff --git a/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/relation_page_links_query_spec.rb b/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/relation_page_links_query_spec.rb index 694f9763867..e6f79d26523 100644 --- a/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/relation_page_links_query_spec.rb +++ b/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/relation_page_links_query_spec.rb @@ -35,7 +35,7 @@ RSpec.describe Wikis::Adapters::Providers::Internal::Queries::RelationPageLinks let(:provider) { create(:internal_wiki_provider) } let(:input_data) { Wikis::Adapters::Input::RelationPageLinks.build(linkable: work_package).value! } - let(:auth_strategy) { provider.auth_strategy_for(current_user).value! } + let(:auth_strategy) { provider.auth_strategy_for(user).value! } let(:wiki_page) { create(:wiki_page) } let(:project) { wiki_page.project } @@ -48,10 +48,10 @@ RSpec.describe Wikis::Adapters::Providers::Internal::Queries::RelationPageLinks create(:relation_wiki_page_link, provider:, linkable: work_package, identifier: "THIS IS NO MOON") end - current_user { create(:user) } + let(:user) { create(:user) } before do - create(:member, project:, user: current_user, roles: [create(:project_role, permissions:)]) + create(:member, project:, user:, roles: [create(:project_role, permissions:)]) link_to_existing_page link_to_non_existing_page diff --git a/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/search_pages_spec.rb b/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/search_pages_spec.rb index 4cf616a7ee4..9717746432e 100644 --- a/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/search_pages_spec.rb +++ b/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/search_pages_spec.rb @@ -35,18 +35,18 @@ RSpec.describe Wikis::Adapters::Providers::Internal::Queries::SearchPages do let(:provider) { create(:internal_wiki_provider) } let(:input_data) { Wikis::Adapters::Input::SearchPages.build(query:).value! } - let(:auth_strategy) { provider.auth_strategy_for(current_user).value! } + let(:auth_strategy) { provider.auth_strategy_for(user).value! } let(:query) { wiki_page.title } let(:wiki_page) { create(:wiki_page, title: "Wiki Page with a Title you will love") } let(:wiki_project) { wiki_page.project } let(:wiki_project_permissions) { %i[view_wiki_pages] } - current_user { create(:user) } + let(:user) { create(:user) } before do create(:member, project: wiki_project, - user: current_user, + user:, roles: [create(:project_role, permissions: wiki_project_permissions)]) wiki_page