diff --git a/modules/wikis/app/models/wikis/provider.rb b/modules/wikis/app/models/wikis/provider.rb index dacb60f3639..670bfceecde 100644 --- a/modules/wikis/app/models/wikis/provider.rb +++ b/modules/wikis/app/models/wikis/provider.rb @@ -65,6 +65,10 @@ module Wikis Adapters::Registry["#{self.class.registry_prefix}.#{registry_path}"].new(model: self, **init_options) end + def inspect + "#<#{self.class.name} id: #{id} name: #{name}>" + end + private def generate_universal_identifier diff --git a/modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/concerns/xwiki_query.rb b/modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/concerns/xwiki_query.rb new file mode 100644 index 00000000000..1a317968ab9 --- /dev/null +++ b/modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/concerns/xwiki_query.rb @@ -0,0 +1,80 @@ +# 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 + module Concerns + module XWikiQuery + ACCEPT_HEADERS = { "Accept" => "application/json" }.freeze + + def authenticated(auth_strategy) + Adapters::Authentication[auth_strategy].call do |http| + yield http.with(headers: ACCEPT_HEADERS) + end + end + + def rest_url(path, query: nil) + # TODO: we might be able to extract a common URL formatting helper from Storages::UrlBuilder + url = "#{provider.url.chomp('/')}/rest/#{path.delete_prefix('/')}" + return url if query.nil? + + "#{url}?#{query.to_query}" + end + + def handle_response(response) + return failure(code: :connection_error) if response.is_a?(HTTPX::ErrorResponse) + + case response + in { status: 200..299 } + begin + json = response.json + rescue MultiJson::ParseError + return failure(code: :invalid_response) + end + + yield json + in { status: 401 | 403 } + failure(code: :unauthorized) + in { status: 404 } + failure(code: :not_found) + else + failure(code: :request_failed) + end + end + end + end + end + end + end + end +end diff --git a/modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/page_info.rb b/modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/page_info.rb index de377d2c359..53a25638026 100644 --- a/modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/page_info.rb +++ b/modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/page_info.rb @@ -34,51 +34,25 @@ module Wikis module XWiki module Queries class PageInfo < BaseQuery - ACCEPT_HEADERS = { "Accept" => "application/json" }.freeze + include Concerns::XWikiQuery def call(input_data:, auth_strategy:) ref = PageReference.parse(input_data.identifier) return failure(code: :not_found) unless ref - url = "#{provider.url.chomp('/')}/rest#{ref.rest_path}" - Adapters::Authentication[auth_strategy].call do |http| - handle_response( - http.with(headers: ACCEPT_HEADERS).get(url), - identifier: input_data.identifier - ) + authenticated(auth_strategy) do |http| + handle_response(http.get(rest_url(ref.rest_path))) do |data| + success( + Results::PageInfo.new( + identifier: input_data.identifier, + title: data["title"], + href: data["xwikiAbsoluteUrl"], + provider: + ) + ) + end end end - - private - - def handle_response(response, identifier:) - return failure(code: :connection_error) if response.is_a?(HTTPX::ErrorResponse) - - case response - in { status: 200..299 } - handle_success_response(response, identifier:) - in { status: 401 | 403 } - failure(code: :unauthorized) - in { status: 404 } - failure(code: :not_found) - else - failure(code: :request_failed) - end - end - - def handle_success_response(response, identifier:) - data = response.json - success( - Results::PageInfo.new( - identifier:, - title: data["title"], - href: data["xwikiAbsoluteUrl"], - provider: - ) - ) - rescue MultiJson::ParseError - failure(code: :invalid_response) - end 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 index bd02594e1f6..5c09cabb327 100644 --- 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 @@ -34,16 +34,33 @@ module Wikis module XWiki module Queries class SearchPages < BaseQuery - def call(input_data:, **) - # TODO: use real API endpoints once available + include Concerns::XWikiQuery - titles = [ - "#{input_data.query} makes XWiki special", - "API documentation of #{input_data.query}", - "A brief introduction on configuring your own #{input_data.query}." - ] + MAXIMUM_RESULTS = 50 - success(titles.map { Results::PageInfo.new(identifier: "1338", title: it, href: "#", provider:) }) + def call(input_data:, auth_strategy:) + query = { q: "\"#{escape_quotes input_data.query}\"", number: MAXIMUM_RESULTS } + + authenticated(auth_strategy) do |http| + handle_response(http.get(rest_url("wikis/query", query:))) do |json| + success( + json.fetch("searchResults") + .uniq { |r| r.fetch("id") } + .map do |r| + result = page_info(identifier: r.fetch("id"), auth_strategy:) + return result if result.failure? + + result.value! + end + ) + end + end + end + + private + + def escape_quotes(string) + string.gsub("\\", "\\\\").gsub('"', '\"') end end end diff --git a/modules/wikis/spec/factories/wiki_provider_factory.rb b/modules/wikis/spec/factories/wiki_provider_factory.rb index f533f01a4f7..5ad90b75c97 100644 --- a/modules/wikis/spec/factories/wiki_provider_factory.rb +++ b/modules/wikis/spec/factories/wiki_provider_factory.rb @@ -43,9 +43,16 @@ FactoryBot.define do factory :xwiki_provider, class: "Wikis::XWikiProvider", parent: :wiki_provider do url { "https://xwiki.example.com/" } + transient do + oauth_client_id { "openproject-#{SecureRandom.hex(8)}" } + oauth_client_secret { SecureRandom.alphanumeric(20) } + end + trait :with_oauth_client do - after(:create) do |provider, _| - create(:oauth_client, integration: provider) + after(:create) do |provider, evaluator| + create :oauth_client, integration: provider, + client_id: evaluator.oauth_client_id, + client_secret: evaluator.oauth_client_secret end end @@ -65,6 +72,15 @@ FactoryBot.define do end end + trait :for_local_connection do + with_connected_user + + url { "https://xwiki.local" } + connected_user_token { ENV.fetch("XWIKI_LOCAL_OAUTH_CLIENT_ACCESS_TOKEN", "TOKEN_NOT_CONFIGURED") } + oauth_client_id { ENV.fetch("XWIKI_LOCAL_OAUTH_CLIENT_ID", "CLIENT_ID_NOT_CONFIGURED") } + oauth_client_secret { ENV.fetch("XWIKI_LOCAL_OAUTH_CLIENT_SECRET", "CLIENT_SECRET_NOT_CONFIGURED") } + end + trait :with_oauth_configured do after(:create) do |provider, _evaluator| create(:oauth_client, integration: provider) diff --git a/modules/wikis/spec/services/wikis/adapters/providers/xwiki/queries/search_pages_spec.rb b/modules/wikis/spec/services/wikis/adapters/providers/xwiki/queries/search_pages_spec.rb new file mode 100644 index 00000000000..af265d20a56 --- /dev/null +++ b/modules/wikis/spec/services/wikis/adapters/providers/xwiki/queries/search_pages_spec.rb @@ -0,0 +1,111 @@ +# 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::XWiki::Queries::SearchPages, :webmock do + subject { described_class.new(model: provider).call(input_data:, auth_strategy:) } + + let(:provider) { create(:xwiki_provider, :for_local_connection, connected_user: user) } + let(:input_data) { Wikis::Adapters::Input::SearchPages.build(query:).value! } + let(:auth_strategy) { provider.auth_strategy_for(user).value! } + + let(:user) { create(:user) } + + # Before recording VCR cassettes of this, ensure pages with the following titles exist in XWiki: + # * Test Page for RSpec + # * "Quoted" pages can be tricky + + context "when there are exactly matching pages", vcr: "xwiki/query_exact_match" do + let(:query) { "Test Page for RSpec" } + + 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("Test Page for RSpec") + end + + it "returns no other random results" do + expect(subject.value!.count).to eq(1) + end + end + + context "when there are partially matching pages", vcr: "xwiki/query_partial_match" do + let(:query) { "for RSpec" } + + 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("Test Page for RSpec") + end + + it "returns no other random results" do + expect(subject.value!.count).to eq(1) + end + end + + context "when the searched page contains quotes", vcr: "xwiki/query_quoted_match" do + let(:query) { '"Quoted" pages can be tricky' } + + 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('"Quoted" pages can be tricky') + end + + it "returns no other random results" do + expect(subject.value!.count).to eq(1) + end + + context "and the query omits the quotes", vcr: "xwiki/query_unquoted_match" do + let(:query) { "Quoted pages can be tricky" } + + 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('"Quoted" pages can be tricky') + end + end + end + + context "when there are no matching pages", vcr: "xwiki/query_no_match" do + let(:query) { "A page that does not exist" } + + it { is_expected.to be_success } + + it "returns an empty result" do + expect(subject.value!).to eq([]) + end + end +end diff --git a/spec/support/fixtures/vcr_cassettes/xwiki/query_exact_match.yml b/spec/support/fixtures/vcr_cassettes/xwiki/query_exact_match.yml new file mode 100644 index 00000000000..18637472313 --- /dev/null +++ b/spec/support/fixtures/vcr_cassettes/xwiki/query_exact_match.yml @@ -0,0 +1,94 @@ +--- +http_interactions: +- request: + method: get + uri: https://xwiki.local/rest/wikis/query?number=50&q=%22Test%20Page%20for%20RSpec%22 + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - OpenProject 17.6.0 HTTPX Client + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer + response: + status: + code: 200 + message: OK + headers: + Content-Language: + - en + Content-Script-Type: + - text/javascript + Content-Type: + - application/json;charset=UTF-8 + Date: + - Tue, 02 Jun 2026 08:17:22 GMT + Set-Cookie: + - JSESSIONID=A90934AE8F68AA76D0E0970863A314F8; Path=/; HttpOnly + Xwiki-Form-Token: + - 2XZUUfWArYhRgLYx4nHjhQ + Xwiki-User: + - xwiki:XWiki.admin + Xwiki-Version: + - 18.3.0 + Content-Length: + - '790' + body: + encoding: UTF-8 + string: '{"links":[],"searchResults":[{"links":[{"href":"https://xwiki.local/rest/wikis/xwiki/spaces/Test%20Page/pages/WebHome","rel":"http://www.xwiki.org/rel/page","type":null,"hrefLang":null}],"type":"page","id":"xwiki:Test + Page.WebHome","pageFullName":"Test Page.WebHome","title":"Test Page for RSpec","wiki":"xwiki","space":"Test + Page","pageName":"WebHome","modified":1780386902000,"author":"xwiki:XWiki.admin","authorName":null,"version":"4.1","language":null,"className":null,"objectNumber":null,"filename":null,"score":30.283539,"object":null,"hierarchy":null}],"template":"https://xwiki.local/rest/?q={solrquery}(&number={number})(&start={start})(&orderField={fieldname}(&order={asc|desc}))(&distinct=1)(&prettyNames={false|true})(&wikis={wikis})(&className={classname})"}' + recorded_at: Tue, 02 Jun 2026 08:17:22 GMT +- request: + method: get + uri: https://xwiki.local/rest/wikis/xwiki/spaces/Test%20Page/pages/WebHome + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - OpenProject 17.6.0 HTTPX Client + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer + response: + status: + code: 200 + message: OK + headers: + Content-Language: + - en + Content-Script-Type: + - text/javascript + Content-Type: + - application/json;charset=UTF-8 + Date: + - Tue, 02 Jun 2026 08:17:22 GMT + Set-Cookie: + - JSESSIONID=6D38F35499AE562F0EEE91795F9951CF; Path=/; HttpOnly + Xwiki-Form-Token: + - 2XZUUfWArYhRgLYx4nHjhQ + Xwiki-User: + - xwiki:XWiki.admin + Xwiki-Version: + - 18.3.0 + Content-Length: + - '2224' + body: + encoding: UTF-8 + string: '{"links":[{"href":"https://xwiki.local/rest/wikis/xwiki/spaces/Test%20Page","rel":"http://www.xwiki.org/rel/space","type":null,"hrefLang":null},{"href":"https://xwiki.local/rest/wikis/xwiki/spaces/Test%20Page/pages/WebHome","rel":"http://www.xwiki.org/rel/parent","type":null,"hrefLang":null},{"href":"https://xwiki.local/rest/wikis/xwiki/spaces/Test%20Page/pages/WebHome/history","rel":"http://www.xwiki.org/rel/history","type":null,"hrefLang":null},{"href":"https://xwiki.local/rest/syntaxes","rel":"http://www.xwiki.org/rel/syntaxes","type":null,"hrefLang":null},{"href":"https://xwiki.local/rest/wikis/xwiki/spaces/Test%20Page/pages/WebHome","rel":"self","type":null,"hrefLang":null},{"href":"https://xwiki.local/rest/wikis/xwiki/classes/Test%20Page.WebHome","rel":"http://www.xwiki.org/rel/class","type":null,"hrefLang":null}],"id":"xwiki:Test + Page.WebHome","fullName":"Test Page.WebHome","wiki":"xwiki","space":"Test + Page","name":"WebHome","title":"Test Page for RSpec","rawTitle":"Test Page + for RSpec","parent":"Main.WebHome","parentId":"xwiki:Main.WebHome","version":"4.1","author":"XWiki.admin","authorName":null,"xwikiRelativeUrl":"https://xwiki.local/bin/view/Test%20Page/","xwikiAbsoluteUrl":"https://xwiki.local/bin/view/Test%20Page/","translations":{"links":[],"translations":[],"default":"en"},"syntax":"xwiki/2.1","language":"","majorVersion":4,"minorVersion":1,"hidden":false,"enforceRequiredRights":false,"created":1780383689000,"creator":"XWiki.admin","creatorName":null,"modified":1780386902000,"modifier":"XWiki.admin","modifierName":null,"originalMetadataAuthor":"xwiki:XWiki.admin","originalMetadataAuthorName":null,"comment":"","content":"This + is a test page that I created with my own hands.","clazz":null,"objects":null,"attachments":null,"hierarchy":{"items":[{"label":"xwiki","name":"xwiki","type":"wiki","url":"https://xwiki.local/bin/view/Main/"},{"label":"Test + Page","name":"Test Page","type":"space","url":"https://xwiki.local/bin/view/Test%20Page/"},{"label":"WebHome","name":"WebHome","type":"document","url":"https://xwiki.local/bin/view/Test%20Page/"}]},"rights":[],"renderedContent":null}' + recorded_at: Tue, 02 Jun 2026 08:17:22 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/support/fixtures/vcr_cassettes/xwiki/query_no_match.yml b/spec/support/fixtures/vcr_cassettes/xwiki/query_no_match.yml new file mode 100644 index 00000000000..21d38b10029 --- /dev/null +++ b/spec/support/fixtures/vcr_cassettes/xwiki/query_no_match.yml @@ -0,0 +1,45 @@ +--- +http_interactions: +- request: + method: get + uri: https://xwiki.local/rest/wikis/query?number=50&q=%22A%20page%20that%20does%20not%20exist%22 + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - OpenProject 17.6.0 HTTPX Client + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer + response: + status: + code: 200 + message: OK + headers: + Content-Language: + - en + Content-Script-Type: + - text/javascript + Content-Type: + - application/json;charset=UTF-8 + Date: + - Tue, 02 Jun 2026 08:17:23 GMT + Set-Cookie: + - JSESSIONID=9D417B4F0A9D4C117C903A54394DAD6C; Path=/; HttpOnly + Xwiki-Form-Token: + - 2XZUUfWArYhRgLYx4nHjhQ + Xwiki-User: + - xwiki:XWiki.admin + Xwiki-Version: + - 18.3.0 + Content-Length: + - '251' + body: + encoding: UTF-8 + string: '{"links":[],"searchResults":[],"template":"https://xwiki.local/rest/?q={solrquery}(&number={number})(&start={start})(&orderField={fieldname}(&order={asc|desc}))(&distinct=1)(&prettyNames={false|true})(&wikis={wikis})(&className={classname})"}' + recorded_at: Tue, 02 Jun 2026 08:17:23 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/support/fixtures/vcr_cassettes/xwiki/query_partial_match.yml b/spec/support/fixtures/vcr_cassettes/xwiki/query_partial_match.yml new file mode 100644 index 00000000000..86708b6d8f9 --- /dev/null +++ b/spec/support/fixtures/vcr_cassettes/xwiki/query_partial_match.yml @@ -0,0 +1,94 @@ +--- +http_interactions: +- request: + method: get + uri: https://xwiki.local/rest/wikis/query?number=50&q=%22for%20RSpec%22 + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - OpenProject 17.6.0 HTTPX Client + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer + response: + status: + code: 200 + message: OK + headers: + Content-Language: + - en + Content-Script-Type: + - text/javascript + Content-Type: + - application/json;charset=UTF-8 + Date: + - Tue, 02 Jun 2026 08:17:22 GMT + Set-Cookie: + - JSESSIONID=4570EBB986F92482503F494102024098; Path=/; HttpOnly + Xwiki-Form-Token: + - 2XZUUfWArYhRgLYx4nHjhQ + Xwiki-User: + - xwiki:XWiki.admin + Xwiki-Version: + - 18.3.0 + Content-Length: + - '790' + body: + encoding: UTF-8 + string: '{"links":[],"searchResults":[{"links":[{"href":"https://xwiki.local/rest/wikis/xwiki/spaces/Test%20Page/pages/WebHome","rel":"http://www.xwiki.org/rel/page","type":null,"hrefLang":null}],"type":"page","id":"xwiki:Test + Page.WebHome","pageFullName":"Test Page.WebHome","title":"Test Page for RSpec","wiki":"xwiki","space":"Test + Page","pageName":"WebHome","modified":1780386902000,"author":"xwiki:XWiki.admin","authorName":null,"version":"4.1","language":null,"className":null,"objectNumber":null,"filename":null,"score":14.608791,"object":null,"hierarchy":null}],"template":"https://xwiki.local/rest/?q={solrquery}(&number={number})(&start={start})(&orderField={fieldname}(&order={asc|desc}))(&distinct=1)(&prettyNames={false|true})(&wikis={wikis})(&className={classname})"}' + recorded_at: Tue, 02 Jun 2026 08:17:22 GMT +- request: + method: get + uri: https://xwiki.local/rest/wikis/xwiki/spaces/Test%20Page/pages/WebHome + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - OpenProject 17.6.0 HTTPX Client + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer + response: + status: + code: 200 + message: OK + headers: + Content-Language: + - en + Content-Script-Type: + - text/javascript + Content-Type: + - application/json;charset=UTF-8 + Date: + - Tue, 02 Jun 2026 08:17:22 GMT + Set-Cookie: + - JSESSIONID=36828A84C85C858E144AA581B1A1F138; Path=/; HttpOnly + Xwiki-Form-Token: + - 2XZUUfWArYhRgLYx4nHjhQ + Xwiki-User: + - xwiki:XWiki.admin + Xwiki-Version: + - 18.3.0 + Content-Length: + - '2224' + body: + encoding: UTF-8 + string: '{"links":[{"href":"https://xwiki.local/rest/wikis/xwiki/spaces/Test%20Page","rel":"http://www.xwiki.org/rel/space","type":null,"hrefLang":null},{"href":"https://xwiki.local/rest/wikis/xwiki/spaces/Test%20Page/pages/WebHome","rel":"http://www.xwiki.org/rel/parent","type":null,"hrefLang":null},{"href":"https://xwiki.local/rest/wikis/xwiki/spaces/Test%20Page/pages/WebHome/history","rel":"http://www.xwiki.org/rel/history","type":null,"hrefLang":null},{"href":"https://xwiki.local/rest/syntaxes","rel":"http://www.xwiki.org/rel/syntaxes","type":null,"hrefLang":null},{"href":"https://xwiki.local/rest/wikis/xwiki/spaces/Test%20Page/pages/WebHome","rel":"self","type":null,"hrefLang":null},{"href":"https://xwiki.local/rest/wikis/xwiki/classes/Test%20Page.WebHome","rel":"http://www.xwiki.org/rel/class","type":null,"hrefLang":null}],"id":"xwiki:Test + Page.WebHome","fullName":"Test Page.WebHome","wiki":"xwiki","space":"Test + Page","name":"WebHome","title":"Test Page for RSpec","rawTitle":"Test Page + for RSpec","parent":"Main.WebHome","parentId":"xwiki:Main.WebHome","version":"4.1","author":"XWiki.admin","authorName":null,"xwikiRelativeUrl":"https://xwiki.local/bin/view/Test%20Page/","xwikiAbsoluteUrl":"https://xwiki.local/bin/view/Test%20Page/","translations":{"links":[],"translations":[],"default":"en"},"syntax":"xwiki/2.1","language":"","majorVersion":4,"minorVersion":1,"hidden":false,"enforceRequiredRights":false,"created":1780383689000,"creator":"XWiki.admin","creatorName":null,"modified":1780386902000,"modifier":"XWiki.admin","modifierName":null,"originalMetadataAuthor":"xwiki:XWiki.admin","originalMetadataAuthorName":null,"comment":"","content":"This + is a test page that I created with my own hands.","clazz":null,"objects":null,"attachments":null,"hierarchy":{"items":[{"label":"xwiki","name":"xwiki","type":"wiki","url":"https://xwiki.local/bin/view/Main/"},{"label":"Test + Page","name":"Test Page","type":"space","url":"https://xwiki.local/bin/view/Test%20Page/"},{"label":"WebHome","name":"WebHome","type":"document","url":"https://xwiki.local/bin/view/Test%20Page/"}]},"rights":[],"renderedContent":null}' + recorded_at: Tue, 02 Jun 2026 08:17:22 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/support/fixtures/vcr_cassettes/xwiki/query_quoted_match.yml b/spec/support/fixtures/vcr_cassettes/xwiki/query_quoted_match.yml new file mode 100644 index 00000000000..bc3063416a4 --- /dev/null +++ b/spec/support/fixtures/vcr_cassettes/xwiki/query_quoted_match.yml @@ -0,0 +1,94 @@ +--- +http_interactions: +- request: + method: get + uri: https://xwiki.local/rest/wikis/query?number=50&q=%22%5C%22Quoted%5C%22%20pages%20can%20be%20tricky%22 + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - OpenProject 17.6.0 HTTPX Client + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer + response: + status: + code: 200 + message: OK + headers: + Content-Language: + - en + Content-Script-Type: + - text/javascript + Content-Type: + - application/json;charset=UTF-8 + Date: + - Tue, 02 Jun 2026 08:17:22 GMT + Set-Cookie: + - JSESSIONID=C28E0999BF663CA702569EE1C5C696E2; Path=/; HttpOnly + Xwiki-Form-Token: + - 2XZUUfWArYhRgLYx4nHjhQ + Xwiki-User: + - xwiki:XWiki.admin + Xwiki-Version: + - 18.3.0 + Content-Length: + - '893' + body: + encoding: UTF-8 + string: '{"links":[],"searchResults":[{"links":[{"href":"https://xwiki.local/rest/wikis/xwiki/spaces/%22Quoted%22%20pages%20can%20be%20tricky/pages/WebHome","rel":"http://www.xwiki.org/rel/page","type":null,"hrefLang":null}],"type":"page","id":"xwiki:\"Quoted\" + pages can be tricky.WebHome","pageFullName":"\"Quoted\" pages can be tricky.WebHome","title":"\"Quoted\" + pages can be tricky","wiki":"xwiki","space":"\"Quoted\" pages can be tricky","pageName":"WebHome","modified":1780387197000,"author":"xwiki:XWiki.admin","authorName":null,"version":"1.1","language":null,"className":null,"objectNumber":null,"filename":null,"score":42.605682,"object":null,"hierarchy":null}],"template":"https://xwiki.local/rest/?q={solrquery}(&number={number})(&start={start})(&orderField={fieldname}(&order={asc|desc}))(&distinct=1)(&prettyNames={false|true})(&wikis={wikis})(&className={classname})"}' + recorded_at: Tue, 02 Jun 2026 08:17:22 GMT +- request: + method: get + uri: https://xwiki.local/rest/wikis/xwiki/spaces/%22Quoted%22%20pages%20can%20be%20tricky/pages/WebHome + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - OpenProject 17.6.0 HTTPX Client + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer + response: + status: + code: 200 + message: OK + headers: + Content-Language: + - en + Content-Script-Type: + - text/javascript + Content-Type: + - application/json;charset=UTF-8 + Date: + - Tue, 02 Jun 2026 08:17:22 GMT + Set-Cookie: + - JSESSIONID=EC142E0079C91196143B051273FDA2FD; Path=/; HttpOnly + Xwiki-Form-Token: + - 2XZUUfWArYhRgLYx4nHjhQ + Xwiki-User: + - xwiki:XWiki.admin + Xwiki-Version: + - 18.3.0 + Content-Length: + - '2642' + body: + encoding: UTF-8 + string: '{"links":[{"href":"https://xwiki.local/rest/wikis/xwiki/spaces/%22Quoted%22%20pages%20can%20be%20tricky","rel":"http://www.xwiki.org/rel/space","type":null,"hrefLang":null},{"href":"https://xwiki.local/rest/wikis/xwiki/spaces/%22Quoted%22%20pages%20can%20be%20tricky/pages/WebHome","rel":"http://www.xwiki.org/rel/parent","type":null,"hrefLang":null},{"href":"https://xwiki.local/rest/wikis/xwiki/spaces/%22Quoted%22%20pages%20can%20be%20tricky/pages/WebHome/history","rel":"http://www.xwiki.org/rel/history","type":null,"hrefLang":null},{"href":"https://xwiki.local/rest/syntaxes","rel":"http://www.xwiki.org/rel/syntaxes","type":null,"hrefLang":null},{"href":"https://xwiki.local/rest/wikis/xwiki/spaces/%22Quoted%22%20pages%20can%20be%20tricky/pages/WebHome","rel":"self","type":null,"hrefLang":null},{"href":"https://xwiki.local/rest/wikis/xwiki/classes/%22Quoted%22%20pages%20can%20be%20tricky.WebHome","rel":"http://www.xwiki.org/rel/class","type":null,"hrefLang":null}],"id":"xwiki:\"Quoted\" + pages can be tricky.WebHome","fullName":"\"Quoted\" pages can be tricky.WebHome","wiki":"xwiki","space":"\"Quoted\" + pages can be tricky","name":"WebHome","title":"\"Quoted\" pages can be tricky","rawTitle":"\"Quoted\" + pages can be tricky","parent":"Main.WebHome","parentId":"xwiki:Main.WebHome","version":"1.1","author":"XWiki.admin","authorName":null,"xwikiRelativeUrl":"https://xwiki.local/bin/view/%22Quoted%22%20pages%20can%20be%20tricky/","xwikiAbsoluteUrl":"https://xwiki.local/bin/view/%22Quoted%22%20pages%20can%20be%20tricky/","translations":{"links":[],"translations":[],"default":"en"},"syntax":"xwiki/2.1","language":"","majorVersion":1,"minorVersion":1,"hidden":false,"enforceRequiredRights":false,"created":1780387197000,"creator":"XWiki.admin","creatorName":null,"modified":1780387197000,"modifier":"XWiki.admin","modifierName":null,"originalMetadataAuthor":"xwiki:XWiki.admin","originalMetadataAuthorName":null,"comment":"","content":"When + the page title contains quotes, it''s harder to exactly match their page title.","clazz":null,"objects":null,"attachments":null,"hierarchy":{"items":[{"label":"xwiki","name":"xwiki","type":"wiki","url":"https://xwiki.local/bin/view/Main/"},{"label":"\"Quoted\" + pages can be tricky","name":"\"Quoted\" pages can be tricky","type":"space","url":"https://xwiki.local/bin/view/%22Quoted%22%20pages%20can%20be%20tricky/"},{"label":"WebHome","name":"WebHome","type":"document","url":"https://xwiki.local/bin/view/%22Quoted%22%20pages%20can%20be%20tricky/"}]},"rights":[],"renderedContent":null}' + recorded_at: Tue, 02 Jun 2026 08:17:22 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/support/fixtures/vcr_cassettes/xwiki/query_unquoted_match.yml b/spec/support/fixtures/vcr_cassettes/xwiki/query_unquoted_match.yml new file mode 100644 index 00000000000..db53a2fc824 --- /dev/null +++ b/spec/support/fixtures/vcr_cassettes/xwiki/query_unquoted_match.yml @@ -0,0 +1,94 @@ +--- +http_interactions: +- request: + method: get + uri: https://xwiki.local/rest/wikis/query?number=50&q=%22Quoted%20pages%20can%20be%20tricky%22 + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - OpenProject 17.6.0 HTTPX Client + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer + response: + status: + code: 200 + message: OK + headers: + Content-Language: + - en + Content-Script-Type: + - text/javascript + Content-Type: + - application/json;charset=UTF-8 + Date: + - Tue, 02 Jun 2026 08:17:23 GMT + Set-Cookie: + - JSESSIONID=10D557C0A4C7D3C599B80C2FA01F5FF5; Path=/; HttpOnly + Xwiki-Form-Token: + - 2XZUUfWArYhRgLYx4nHjhQ + Xwiki-User: + - xwiki:XWiki.admin + Xwiki-Version: + - 18.3.0 + Content-Length: + - '893' + body: + encoding: UTF-8 + string: '{"links":[],"searchResults":[{"links":[{"href":"https://xwiki.local/rest/wikis/xwiki/spaces/%22Quoted%22%20pages%20can%20be%20tricky/pages/WebHome","rel":"http://www.xwiki.org/rel/page","type":null,"hrefLang":null}],"type":"page","id":"xwiki:\"Quoted\" + pages can be tricky.WebHome","pageFullName":"\"Quoted\" pages can be tricky.WebHome","title":"\"Quoted\" + pages can be tricky","wiki":"xwiki","space":"\"Quoted\" pages can be tricky","pageName":"WebHome","modified":1780387197000,"author":"xwiki:XWiki.admin","authorName":null,"version":"1.1","language":null,"className":null,"objectNumber":null,"filename":null,"score":42.605682,"object":null,"hierarchy":null}],"template":"https://xwiki.local/rest/?q={solrquery}(&number={number})(&start={start})(&orderField={fieldname}(&order={asc|desc}))(&distinct=1)(&prettyNames={false|true})(&wikis={wikis})(&className={classname})"}' + recorded_at: Tue, 02 Jun 2026 08:17:23 GMT +- request: + method: get + uri: https://xwiki.local/rest/wikis/xwiki/spaces/%22Quoted%22%20pages%20can%20be%20tricky/pages/WebHome + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - OpenProject 17.6.0 HTTPX Client + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer + response: + status: + code: 200 + message: OK + headers: + Content-Language: + - en + Content-Script-Type: + - text/javascript + Content-Type: + - application/json;charset=UTF-8 + Date: + - Tue, 02 Jun 2026 08:17:23 GMT + Set-Cookie: + - JSESSIONID=85112D2673FA7F3B99AEC6BF0A182372; Path=/; HttpOnly + Xwiki-Form-Token: + - 2XZUUfWArYhRgLYx4nHjhQ + Xwiki-User: + - xwiki:XWiki.admin + Xwiki-Version: + - 18.3.0 + Content-Length: + - '2642' + body: + encoding: UTF-8 + string: '{"links":[{"href":"https://xwiki.local/rest/wikis/xwiki/spaces/%22Quoted%22%20pages%20can%20be%20tricky","rel":"http://www.xwiki.org/rel/space","type":null,"hrefLang":null},{"href":"https://xwiki.local/rest/wikis/xwiki/spaces/%22Quoted%22%20pages%20can%20be%20tricky/pages/WebHome","rel":"http://www.xwiki.org/rel/parent","type":null,"hrefLang":null},{"href":"https://xwiki.local/rest/wikis/xwiki/spaces/%22Quoted%22%20pages%20can%20be%20tricky/pages/WebHome/history","rel":"http://www.xwiki.org/rel/history","type":null,"hrefLang":null},{"href":"https://xwiki.local/rest/syntaxes","rel":"http://www.xwiki.org/rel/syntaxes","type":null,"hrefLang":null},{"href":"https://xwiki.local/rest/wikis/xwiki/spaces/%22Quoted%22%20pages%20can%20be%20tricky/pages/WebHome","rel":"self","type":null,"hrefLang":null},{"href":"https://xwiki.local/rest/wikis/xwiki/classes/%22Quoted%22%20pages%20can%20be%20tricky.WebHome","rel":"http://www.xwiki.org/rel/class","type":null,"hrefLang":null}],"id":"xwiki:\"Quoted\" + pages can be tricky.WebHome","fullName":"\"Quoted\" pages can be tricky.WebHome","wiki":"xwiki","space":"\"Quoted\" + pages can be tricky","name":"WebHome","title":"\"Quoted\" pages can be tricky","rawTitle":"\"Quoted\" + pages can be tricky","parent":"Main.WebHome","parentId":"xwiki:Main.WebHome","version":"1.1","author":"XWiki.admin","authorName":null,"xwikiRelativeUrl":"https://xwiki.local/bin/view/%22Quoted%22%20pages%20can%20be%20tricky/","xwikiAbsoluteUrl":"https://xwiki.local/bin/view/%22Quoted%22%20pages%20can%20be%20tricky/","translations":{"links":[],"translations":[],"default":"en"},"syntax":"xwiki/2.1","language":"","majorVersion":1,"minorVersion":1,"hidden":false,"enforceRequiredRights":false,"created":1780387197000,"creator":"XWiki.admin","creatorName":null,"modified":1780387197000,"modifier":"XWiki.admin","modifierName":null,"originalMetadataAuthor":"xwiki:XWiki.admin","originalMetadataAuthorName":null,"comment":"","content":"When + the page title contains quotes, it''s harder to exactly match their page title.","clazz":null,"objects":null,"attachments":null,"hierarchy":{"items":[{"label":"xwiki","name":"xwiki","type":"wiki","url":"https://xwiki.local/bin/view/Main/"},{"label":"\"Quoted\" + pages can be tricky","name":"\"Quoted\" pages can be tricky","type":"space","url":"https://xwiki.local/bin/view/%22Quoted%22%20pages%20can%20be%20tricky/"},{"label":"WebHome","name":"WebHome","type":"document","url":"https://xwiki.local/bin/view/%22Quoted%22%20pages%20can%20be%20tricky/"}]},"rights":[],"renderedContent":null}' + recorded_at: Tue, 02 Jun 2026 08:17:23 GMT +recorded_with: VCR 6.4.0