From 59782a7232bf88e99c216fc8ea61baa4476402b3 Mon Sep 17 00:00:00 2001 From: Eduardo Navarro Date: Wed, 26 Nov 2025 18:10:20 +0100 Subject: [PATCH 1/7] Use the `Backend::Api` library to update the link of a package... ... instead of using direct calls to `Backend::Connection` --- src/api/app/helpers/maintenance_helper.rb | 10 ++++------ src/api/app/lib/backend/api/sources/package.rb | 6 ++++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/api/app/helpers/maintenance_helper.rb b/src/api/app/helpers/maintenance_helper.rb index 6d27b49d818..f0a100cd04c 100644 --- a/src/api/app/helpers/maintenance_helper.rb +++ b/src/api/app/helpers/maintenance_helper.rb @@ -148,7 +148,7 @@ def instantiate_container(project, opackage, opts = {}) link_xml = Nokogiri::XML(lpkg.source_file('_link'), &:strict).root link_xml.remove_attribute('project') # its a local link, project name not needed link_xml['package'] = pkg.name - Backend::Connection.put lpkg.source_path('_link', user: User.session!.login), link_xml.to_xml + Backend::Api::Sources::Package.write_link(lpkg.project.name, lpkg.name, User.session!.login, link_xml.to_xml) lpkg.sources_changed end end @@ -346,16 +346,14 @@ def release_package_create_main_package(request, source_package, target_package_ lpkg.store end upload_params = { - user: User.session!.login, rev: 'repository', comment: "Set link to #{target_package_name} via maintenance_release request" } - upload_path = Addressable::URI.escape("/source/#{target_project.name}/#{base_package_name}/_link") - upload_path << Backend::Connection.build_query_from_hash(upload_params, %i[user rev]) link = "\n" md5 = Digest::MD5.hexdigest(link) - Backend::Connection.put upload_path, link + Backend::Api::Sources::Package.write_link(target_project.name, base_package_name, User.session!.login, link, upload_params) # commit + upload_params[:user] = User.session!.login upload_params[:cmd] = 'commitfilelist' upload_params[:noservice] = '1' upload_params[:requestid] = request.number if request @@ -369,7 +367,7 @@ def release_package_relink(link, action, target_package_name, target_project, tp link.remove_attribute('project') # its a local link, project name not needed link['package'] = link['package'].gsub(/\..*/, '') + target_package_name.gsub(/.*\./, '.') # adapt link target with suffix link_xml = link.to_xml - Backend::Connection.put Addressable::URI.escape("/source/#{target_project.name}/#{target_package_name}/_link?rev=repository&user=#{User.session!.login}"), link_xml + Backend::Api::Sources::Package.write_link(target_project.name, target_package_name, User.session!.login, link_xml, { rev: 'repository' }) md5 = Digest::MD5.hexdigest(link_xml) # commit with noservice parameter diff --git a/src/api/app/lib/backend/api/sources/package.rb b/src/api/app/lib/backend/api/sources/package.rb index 7bdad96ac0f..957d701b1b2 100644 --- a/src/api/app/lib/backend/api/sources/package.rb +++ b/src/api/app/lib/backend/api/sources/package.rb @@ -122,8 +122,10 @@ def self.link_info(project, package) # Writes the link information of a package # @return [String] - def self.write_link(project_name, package_name, user_login, content) - http_put(['/source/:project/:package/_link', project_name, package_name], data: content, params: { user: user_login }.compact) + def self.write_link(project_name, package_name, user_login, content, options = {}) + accepted = %i[rev comment] + http_put(['/source/:project/:package/_link', project_name, package_name], + data: content, params: options, defaults: { user: user_login }, accepted: accepted) end # Returns the source diff as UTF-8 encoded string From 2199af581e573db551e32be29dedc24f779d5875 Mon Sep 17 00:00:00 2001 From: Eduardo Navarro Date: Thu, 27 Nov 2025 16:47:55 +0100 Subject: [PATCH 2/7] Rescue ArgumentError exceptions in search API endpoints Do not include detailed parser messages when the raised error is an `ArgumentError`. Keep showing detailed error information for all other previously handled exceptions. --- src/api/app/controllers/search_controller.rb | 8 +++++--- src/api/spec/controllers/search_controller_spec.rb | 11 +++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/api/app/controllers/search_controller.rb b/src/api/app/controllers/search_controller.rb index f5ffd4fbc57..f6b0e0c2e3e 100644 --- a/src/api/app/controllers/search_controller.rb +++ b/src/api/app/controllers/search_controller.rb @@ -284,9 +284,11 @@ def find_attribs(attrib, project_name, package_name) def find_items(what, predicate) XpathEngine.new.find("/#{what}[#{predicate}]") - rescue XpathEngine::IllegalXpathError => e - raise IllegalXpathError, "Error found searching elements '#{what}' with xpath predicate: '#{predicate}'.\n\n" \ - "Detailed error message from parser: #{e.message}" + rescue ArgumentError, XpathEngine::IllegalXpathError => e + message = "Error found searching elements '#{what}' with xpath predicate: '#{predicate}'." + message += "\n\nDetailed error message from parser: #{e.message}" unless e.is_a?(ArgumentError) + + raise IllegalXpathError, message end def search_results_exceed_configured_limit?(matches) diff --git a/src/api/spec/controllers/search_controller_spec.rb b/src/api/spec/controllers/search_controller_spec.rb index 89a21c72b44..b23fc87aa30 100644 --- a/src/api/spec/controllers/search_controller_spec.rb +++ b/src/api/spec/controllers/search_controller_spec.rb @@ -123,6 +123,17 @@ expect(Nokogiri::XML(response.body).xpath('//status/summary').inner_text).to match(%r{Error found searching elements 'request' with xpath predicate: '/e\\u0000'.}) end end + + describe 'wrong number of arguments' do + it 'shows an error', :aggregate_failures do + get :project, params: { match: 'starts-with(openSUSE:Backports)' }, format: :xml + + expect(response).to have_http_status(:bad_request) + expect(Nokogiri::XML(response.body).xpath('//status').attribute('code').value).to eq('illegal_xpath_error') + expect(Nokogiri::XML(response.body).xpath('//status/summary').inner_text) + .to match(/Error found searching elements 'project' with xpath predicate: 'starts-with\(openSUSE:Backports\)'./) + end + end end describe 'search limited to 2 results', vcr: false do From 984d008adcffafbbb5b0a8f9e138961f5de49f62 Mon Sep 17 00:00:00 2001 From: Eduardo Navarro Date: Thu, 27 Nov 2025 09:49:40 +0100 Subject: [PATCH 3/7] Use the `Backend::Api` library to update the commit file list... ... of a package instead of using direct calls to `Backend::Connection` --- src/api/app/helpers/maintenance_helper.rb | 13 +++++-------- src/api/app/lib/backend/api/sources/package.rb | 3 ++- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/api/app/helpers/maintenance_helper.rb b/src/api/app/helpers/maintenance_helper.rb index f0a100cd04c..adbe1027a52 100644 --- a/src/api/app/helpers/maintenance_helper.rb +++ b/src/api/app/helpers/maintenance_helper.rb @@ -354,12 +354,10 @@ def release_package_create_main_package(request, source_package, target_package_ Backend::Api::Sources::Package.write_link(target_project.name, base_package_name, User.session!.login, link, upload_params) # commit upload_params[:user] = User.session!.login - upload_params[:cmd] = 'commitfilelist' upload_params[:noservice] = '1' upload_params[:requestid] = request.number if request - upload_path = Addressable::URI.escape("/source/#{target_project.name}/#{base_package_name}") - upload_path << Backend::Connection.build_query_from_hash(upload_params, %i[user comment cmd noservice requestid]) - answer = Backend::Connection.post upload_path, " " + answer = Backend::Api::Sources::Package.write_filelist(target_project.name, base_package_name, + " ", upload_params) lpkg.sources_changed(dir_xml: answer) end @@ -373,14 +371,13 @@ def release_package_relink(link, action, target_package_name, target_project, tp # commit with noservice parameter upload_params = { user: User.session!.login, - cmd: 'commitfilelist', noservice: '1', comment: "Set local link to #{target_package_name} via maintenance_release request" } upload_params[:requestid] = action.bs_request.number if action - upload_path = Addressable::URI.escape("/source/#{target_project.name}/#{target_package_name}") - upload_path << Backend::Connection.build_query_from_hash(upload_params, %i[user comment cmd noservice requestid]) - answer = Backend::Connection.post upload_path, " " + answer = Backend::Api::Sources::Package.write_filelist(target_project.name, target_package_name, + " ", upload_params) + tpkg.sources_changed(dir_xml: answer) end end diff --git a/src/api/app/lib/backend/api/sources/package.rb b/src/api/app/lib/backend/api/sources/package.rb index 957d701b1b2..7b481faddf3 100644 --- a/src/api/app/lib/backend/api/sources/package.rb +++ b/src/api/app/lib/backend/api/sources/package.rb @@ -164,7 +164,8 @@ def self.rebuild(project_name, package_name, options = {}) # Writes source filelist to the package # @return [String] def self.write_filelist(project_name, package_name, filelist, params = {}) - http_post(['/source/:project/:package', project_name, package_name], defaults: { cmd: :commitfilelist }, data: filelist, params: params) + http_post(['/source/:project/:package', project_name, package_name], + defaults: { cmd: :commitfilelist }, data: filelist, params: params, accepted: %i[user comment rev noservice requestid]) end # Deletes the package and all the source files inside From 409d1ad4be17e15ef0799f4ad785beba15051a4a Mon Sep 17 00:00:00 2001 From: Eduardo Navarro Date: Thu, 27 Nov 2025 10:16:55 +0100 Subject: [PATCH 4/7] Use the `Backend::Api` library to copy the binaries of a package... ... instead of using direct calls to `Backend::Connection` Don't pass the `user` parameter to the backend. This backend endpoint has never accepted the `user` parameter. Adapt the specs to mock `Backend::Api::BuildResults::Binaries` instead of `Backend::Connection`. --- src/api/app/helpers/maintenance_helper.rb | 9 +----- .../lib/backend/api/build_results/binaries.rb | 7 +++++ .../controllers/trigger_controller_spec.rb | 17 +++-------- src/api/spec/models/token/release_spec.rb | 30 ++++++++----------- 4 files changed, 24 insertions(+), 39 deletions(-) diff --git a/src/api/app/helpers/maintenance_helper.rb b/src/api/app/helpers/maintenance_helper.rb index adbe1027a52..a692951bdbc 100644 --- a/src/api/app/helpers/maintenance_helper.rb +++ b/src/api/app/helpers/maintenance_helper.rb @@ -239,22 +239,15 @@ def copy_binaries_to_repository(source_repository, filter_architecture, source_p def copy_single_binary(arch, target_repository, source_project_name, source_package_name, source_repo, target_package_name, update_info_id, setrelease) cp_params = { - cmd: 'copy', oproject: source_project_name, opackage: source_package_name, orepository: source_repo.name, - user: User.session!.login, resign: '1' } cp_params[:setupdateinfoid] = update_info_id if update_info_id cp_params[:setrelease] = setrelease if setrelease cp_params[:multibuild] = '1' unless source_package_name.include?(':') - cp_path = Addressable::URI.escape("/build/#{target_repository.project.name}/#{target_repository.name}/#{arch.name}/#{target_package_name}") - - cp_path << Backend::Connection.build_query_from_hash(cp_params, %i[cmd oproject opackage - orepository setupdateinfoid - resign setrelease multibuild]) - Backend::Connection.post cp_path + Backend::Api::BuildResults::Binaries.copy(target_repository.project.name, target_repository.name, arch.name, target_package_name, cp_params) end def create_package_container_if_missing(source_package, target_package_name, target_project) diff --git a/src/api/app/lib/backend/api/build_results/binaries.rb b/src/api/app/lib/backend/api/build_results/binaries.rb index 76abb7ecee5..e58ef7a49e2 100644 --- a/src/api/app/lib/backend/api/build_results/binaries.rb +++ b/src/api/app/lib/backend/api/build_results/binaries.rb @@ -16,6 +16,13 @@ def self.history(project, repository, package, architecture) http_get(['/build/:project/:repository/:architecture/:package/_history', project, repository, architecture, package]) end + # @return [String] + def self.copy(project_name, repository_name, architecture_name, package_name, options = {}) + accepted = %i[oproject opackage orepository resign setupdateinfoid setrelease multibuild] + http_post(['/build/:project/:repository/:architecture/:package', project_name, repository_name, architecture_name, package_name], + defaults: { cmd: :copy }, params: options, accepted: accepted) + end + # Returns the jobs history for a project # @return [String] def self.job_history(project_name, repository_name, architecture_name) diff --git a/src/api/spec/controllers/trigger_controller_spec.rb b/src/api/spec/controllers/trigger_controller_spec.rb index cfe42adbd0d..b5579e9cf2c 100644 --- a/src/api/spec/controllers/trigger_controller_spec.rb +++ b/src/api/spec/controllers/trigger_controller_spec.rb @@ -89,16 +89,13 @@ project end let(:source_package) { source_project.packages.first } - let(:backend_url) do - '/build/target_project/target_repository/x86_64/source_package' \ - '?cmd=copy&oproject=source_project&opackage=source_package&orepository=source_repository' \ - '&resign=1&multibuild=1' - end # Mock the cmd=copy HTTP request. Mocking Token::Release/MaintenanceHelper is just too hard... before do - allow(Backend::Connection).to receive(:post).and_call_original - allow(Backend::Connection).to receive(:post).with(backend_url).and_return("\n") + allow(Backend::Api::BuildResults::Binaries).to receive(:copy) + .with('target_project', 'target_repository', 'x86_64', 'source_package', + { multibuild: '1', opackage: 'source_package', oproject: 'source_project', orepository: 'source_repository', resign: '1' }) + .and_return("\n") end context 'with token.package' do @@ -120,12 +117,6 @@ context 'with project parameter' do subject { post :release, params: { project: source_project.name, format: :xml } } - let(:backend_url) do - '/build/target_project/target_repository/x86_64/source_package' \ - '?cmd=copy&oproject=source_project&orepository=source_repository' \ - '&resign=1&multibuild=1' - end - it { expect(subject).to have_http_status(:success) } end end diff --git a/src/api/spec/models/token/release_spec.rb b/src/api/spec/models/token/release_spec.rb index 6f4978e579c..51dc2ab8807 100644 --- a/src/api/spec/models/token/release_spec.rb +++ b/src/api/spec/models/token/release_spec.rb @@ -30,15 +30,12 @@ context 'when a manual release target is set' do let!(:release_target) { create(:release_target, target_repository: target_repository, repository: repository, trigger: 'manual') } - let(:backend_url) do - "/build/#{target_project.name}/#{target_repository.name}/x86_64/#{package.name}" \ - "?cmd=copy&oproject=#{CGI.escape(project_staging.name)}&opackage=#{package.name}&orepository=#{repository.name}" \ - '&resign=1&multibuild=1' - end before do - allow(Backend::Connection).to receive(:post).and_call_original - allow(Backend::Connection).to receive(:post).with(backend_url).and_return("\n") + allow(Backend::Api::BuildResults::Binaries).to receive(:copy) + .with('Bar', 'target_repository', 'x86_64', 'bar_package', + { multibuild: '1', opackage: 'bar_package', oproject: 'Bar:Staging', orepository: 'package_test_repository', resign: '1' }) + .and_return("\n") end it 'triggers the release process in the backend' do @@ -58,16 +55,13 @@ let(:other_target_project) { create(:project, name: 'Baz', maintainer: user) } let(:other_source_repository) { create(:repository, name: 'other_source_repository', architectures: ['x86_64'], project: project_staging) } let(:other_target_repository) { create(:repository, name: 'other_target_repository', architectures: ['x86_64'], project: other_target_project) } - let(:backend_url) do - "/build/#{other_target_project.name}/#{other_target_repository.name}/x86_64/#{package.name}" \ - "?cmd=copy&oproject=#{CGI.escape(project_staging.name)}&opackage=#{package.name}&orepository=#{other_source_repository.name}" \ - '&resign=1&multibuild=1' - end let!(:release_target) { create(:release_target, target_repository: other_target_repository, repository: other_source_repository, trigger: 'manual') } before do - allow(Backend::Connection).to receive(:post).and_call_original - allow(Backend::Connection).to receive(:post).with(backend_url).and_return("\n") + allow(Backend::Api::BuildResults::Binaries).to receive(:copy) + .with('Baz', 'other_target_repository', 'x86_64', 'bar_package', + { multibuild: '1', opackage: 'bar_package', oproject: 'Bar:Staging', orepository: 'other_source_repository', resign: '1' }) + .and_return("\n") end context 'when the target_project, targetrepository, filter_source_repository and arch parameters are provided' do @@ -80,7 +74,7 @@ subject end - expect(Backend::Connection).to have_received(:post).with(backend_url) + expect(Backend::Api::BuildResults::Binaries).to have_received(:copy) end end @@ -95,7 +89,7 @@ it 'does not trigger the release process in the backend' do user.run_as do expect { subject }.to raise_error(Token::Errors::InsufficientPermissionOnTargetRepository, 'no permission to write in project Foo') - expect(Backend::Connection).not_to have_received(:post).with(backend_url) + expect(Backend::Api::BuildResults::Binaries).not_to have_received(:copy) end end end @@ -108,7 +102,7 @@ it 'does not trigger the release process in the backend' do user.run_as do - expect(Backend::Connection).not_to have_received(:post).with(backend_url) + expect(Backend::Api::BuildResults::Binaries).not_to have_received(:copy) end end end @@ -123,7 +117,7 @@ user.run_as do subject end - expect(Backend::Connection).to have_received(:post).with(backend_url) + expect(Backend::Api::BuildResults::Binaries).to have_received(:copy) end end end From 18b85bca3135740f838917c92bd5b32e7d8af485 Mon Sep 17 00:00:00 2001 From: Eduardo Navarro Date: Thu, 27 Nov 2025 13:01:26 +0100 Subject: [PATCH 5/7] Use the `Backend::Api` library to write the channel of a package... ... instead of using direct calls to `Backend::Connection` --- src/api/app/helpers/maintenance_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/app/helpers/maintenance_helper.rb b/src/api/app/helpers/maintenance_helper.rb index a692951bdbc..1c6a19e3bca 100644 --- a/src/api/app/helpers/maintenance_helper.rb +++ b/src/api/app/helpers/maintenance_helper.rb @@ -81,7 +81,7 @@ def import_channel(channel, pkg, target_repo = nil) query = { user: User.session!.login } query[:comment] = 'channel import function' - Backend::Connection.put(pkg.source_path('_channel', query), channel.to_s) + Backend::Api::Sources::File.write(pkg.project.name, pkg.name, '_channel', channel.to_s, query) pkg.sources_changed # enforce updated channel list in database: From 5ae2ffeff19002168b802569fbbe8c451c122828 Mon Sep 17 00:00:00 2001 From: Geetansh Goyal Date: Fri, 28 Nov 2025 08:26:15 +0530 Subject: [PATCH 6/7] notifications API: use show_maximum param for count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If you pass show_maximum in the query, the API now returns up to that many notifications (capped at max_per_page). This fixes the mismatch between docs and code—before, it always used total. Also checked that state and kind filters work as expected. No more notifications_type in docs. See #18910. --- .../controllers/person/notifications_controller.rb | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/api/app/controllers/person/notifications_controller.rb b/src/api/app/controllers/person/notifications_controller.rb index 1ded5b5421a..0286b9d4e48 100644 --- a/src/api/app/controllers/person/notifications_controller.rb +++ b/src/api/app/controllers/person/notifications_controller.rb @@ -14,11 +14,17 @@ class NotificationsController < ApplicationController # GET /my/notifications def index + # If you pass show_maximum as a query parameter, you'll get up to that many notifications (capped at max_per_page). + # This matches what clients expect and fixes the old behavior where it always used total. See issue #18910. @notifications_count = @notifications.count @paged_notifications = @notifications.order(created_at: :desc).page(params[:page]) params[:page] = @paged_notifications.total_pages if @paged_notifications.out_of_range? - params[:show_maximum] ? show_maximum(@notifications) : @paged_notifications + if params[:show_maximum] + show_maximum(@notifications) + else + @paged_notifications + end end def update @@ -55,8 +61,9 @@ def filter_notifications_by_state(notifications, filter_state) end def show_maximum(notifications) - total = notifications.size - notifications.page(params[:page]).per([total, Notification.max_per_page].min) + max = params[:show_maximum].to_i + max = Notification.max_per_page if max <= 0 || max > Notification.max_per_page + notifications.order(created_at: :desc).limit(max) end def set_filter_kind From 9df55f2fefa29d5edb46e564d7624f151b0f3594 Mon Sep 17 00:00:00 2001 From: Geetansh Goyal Date: Fri, 28 Nov 2025 14:37:50 +0530 Subject: [PATCH 7/7] person/notifications: respect show_maximum param - Implement for /my/notifications. - Add specs for default behavior, limited results and capping at Notification.max_per_page. - Defensive filter handling and improved comments. Fixes #18910. --- .circleci/conditional_config.yml | 1 + .ruby-version | 2 +- .vscode/c_cpp_properties.json | 17 +++++++++++++ src/api/.bundle/config | 2 ++ src/api/Gemfile.lock | 2 +- .../person/notifications_controller_spec.rb | 23 ++++++++++++++---- .../vendor/cache/ffi-1.17.2-arm64-darwin.gem | Bin 0 -> 604160 bytes .../cache/nokogiri-1.18.10-arm64-darwin.gem | Bin 0 -> 6551040 bytes src/api/vendor/cache/rantly-2.0.0.gem | Bin 0 -> 18944 bytes src/api/vendor/cache/rantly-3.0.0.gem | Bin 18944 -> 0 bytes 10 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 .vscode/c_cpp_properties.json create mode 100644 src/api/.bundle/config create mode 100644 src/api/vendor/cache/ffi-1.17.2-arm64-darwin.gem create mode 100644 src/api/vendor/cache/nokogiri-1.18.10-arm64-darwin.gem create mode 100644 src/api/vendor/cache/rantly-2.0.0.gem delete mode 100644 src/api/vendor/cache/rantly-3.0.0.gem diff --git a/.circleci/conditional_config.yml b/.circleci/conditional_config.yml index 333428aaf27..3bdfc6c7bb7 100644 --- a/.circleci/conditional_config.yml +++ b/.circleci/conditional_config.yml @@ -71,6 +71,7 @@ aliases: bundle config build.nokogiri --use-system-libraries bundle config build.sassc --disable-march-tune-native bundle config build.nio4r --with-cflags='-Wno-return-type' + bundle config build.xmlhash --with-cflags='-Wno-error=shorten-64-to-32' bundle config set --local path 'vendor/bundle' bundle install --jobs=4 --retry=3 --local diff --git a/.ruby-version b/.ruby-version index 4d9d11cf505..be94e6f53db 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.4.2 +3.2.2 diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 00000000000..08d2123a3bd --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,17 @@ +{ + "configurations": [ + { + "name": "Mac", + "includePath": [ + "${workspaceFolder}/**", + "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/Headers" + ], + "defines": [], + "compilerPath": "/usr/bin/clang", + "cStandard": "c17", + "cppStandard": "c++14", + "intelliSenseMode": "macos-clang-arm64" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/src/api/.bundle/config b/src/api/.bundle/config new file mode 100644 index 00000000000..eb06b6757bb --- /dev/null +++ b/src/api/.bundle/config @@ -0,0 +1,2 @@ +--- +BUNDLE_BUILD__XMLHASH: "--with-cflags='-Wno-error=shorten-64-to-32'" diff --git a/src/api/Gemfile.lock b/src/api/Gemfile.lock index 4a1c32e744a..47bd9c4788f 100644 --- a/src/api/Gemfile.lock +++ b/src/api/Gemfile.lock @@ -414,7 +414,7 @@ GEM zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.2.1) - rantly (3.0.0) + rantly (2.0.0) rbtree (0.4.6) rbtree3 (0.7.1) rdoc (6.16.0) diff --git a/src/api/spec/controllers/person/notifications_controller_spec.rb b/src/api/spec/controllers/person/notifications_controller_spec.rb index edd754f4459..86dc3694e99 100644 --- a/src/api/spec/controllers/person/notifications_controller_spec.rb +++ b/src/api/spec/controllers/person/notifications_controller_spec.rb @@ -24,20 +24,33 @@ describe 'index' do context 'called by authorized user' do - let!(:notifications) { create_list(:notification_for_request, 2, :web_notification, :request_state_change, subscriber: user) } + let!(:notifications) { create_list(:notification_for_request, 5, :web_notification, :request_state_change, subscriber: user) } before do login user - get :index, format: :xml - notifications.each do |notification| notification.projects << user.home_project notification.save end end - it { expect(response).to have_http_status(:success) } - it { expect(response.body).to include('') } + it 'returns all notifications by default' do + get :index, format: :xml + expect(response).to have_http_status(:success) + expect(response.body).to include('') + end + + it 'returns limited notifications when show_maximum is set' do + get :index, params: { format: :xml, show_maximum: 3 } + expect(response).to have_http_status(:success) + expect(response.body).to include('') + end + + it 'caps show_maximum at Notification.max_per_page' do + get :index, params: { format: :xml, show_maximum: 999 } + expect(response).to have_http_status(:success) + expect(response.body).to include("") + end context 'filter by kind' do let!(:notifications) { create_list(:notification_for_request, 2, :web_notification, :request_state_change, subscriber: user, delivered: true) } diff --git a/src/api/vendor/cache/ffi-1.17.2-arm64-darwin.gem b/src/api/vendor/cache/ffi-1.17.2-arm64-darwin.gem new file mode 100644 index 0000000000000000000000000000000000000000..2219753b41ff279980ba6cd047c66fbc27752b22 GIT binary patch literal 604160 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ξ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