Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package openQA for openSUSE:Factory checked 
in at 2026-02-05 17:59:27
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/openQA (Old)
 and      /work/SRC/openSUSE:Factory/.openQA.new.1670 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "openQA"

Thu Feb  5 17:59:27 2026 rev:803 rq:1331112 version:5.1769644379.ef069e9d

Changes:
--------
--- /work/SRC/openSUSE:Factory/openQA/openQA.changes    2026-01-29 
17:48:18.317876959 +0100
+++ /work/SRC/openSUSE:Factory/.openQA.new.1670/openQA.changes  2026-02-05 
18:02:23.164605392 +0100
@@ -2 +2 @@
-Wed Jan 28 15:07:49 UTC 2026 - [email protected]
+Thu Jan 29 06:10:03 UTC 2026 - [email protected]
@@ -4 +4,20 @@
-- Update to version 5.1769603414.6c0fa72e:
+- Update to version 5.1769644379.ef069e9d:
+  * style: Add quotes in openqa-bootstrap
+  * feat: default API key expiration to 1 year, aligning with UI
+  * feat: wrap array in an object in api_key API responses
+  * feat: add API endpoint for deleting API keys
+  * feat: add API endpoint for listing API keys
+  * feat: add API endpoint for creating API keys
+  * fix(openqa-bootstrap): prevent shellcheck warning SC2086
+  * Add dependency on 'file'
+  * refactor: Write code in `JobGroup.pm` in a more compact way
+  * test: Consider `Job.pm` fully covered
+  * test: Add tests for error handling of artefact upload
+  * refactor: Format artefact upload test in a more compact way
+  * test: Add tests for using assigned worker on job status updates
+  * test: Add tests for re-scheduling invalid scheduled product
+  * test: Add tests for querying non-existent scheduled product
+  * refactor: Use more compact coding style in `show_scheduled_product`
+  * refactor: Improve `Mm.pm`
+  * test: Improve tests of multi-machine API
+  * Remove unused module Config::Tiny from dependencies

Old:
----
  openQA-5.1769603414.6c0fa72e.obscpio

New:
----
  openQA-5.1769644379.ef069e9d.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ openQA-client-test.spec ++++++
--- /var/tmp/diff_new_pack.U2tDce/_old  2026-02-05 18:02:26.728754903 +0100
+++ /var/tmp/diff_new_pack.U2tDce/_new  2026-02-05 18:02:26.744755574 +0100
@@ -18,7 +18,7 @@
 
 %define         short_name openQA-client
 Name:           %{short_name}-test
-Version:        5.1769603414.6c0fa72e
+Version:        5.1769644379.ef069e9d
 Release:        0
 Summary:        Test package for %{short_name}
 License:        GPL-2.0-or-later

++++++ openQA-devel-test.spec ++++++
--- /var/tmp/diff_new_pack.U2tDce/_old  2026-02-05 18:02:27.032767656 +0100
+++ /var/tmp/diff_new_pack.U2tDce/_new  2026-02-05 18:02:27.048768327 +0100
@@ -18,7 +18,7 @@
 
 %define         short_name openQA-devel
 Name:           %{short_name}-test
-Version:        5.1769603414.6c0fa72e
+Version:        5.1769644379.ef069e9d
 Release:        0
 Summary:        Test package for %{short_name}
 License:        GPL-2.0-or-later

++++++ openQA-test.spec ++++++
--- /var/tmp/diff_new_pack.U2tDce/_old  2026-02-05 18:02:27.388782590 +0100
+++ /var/tmp/diff_new_pack.U2tDce/_new  2026-02-05 18:02:27.408783429 +0100
@@ -18,7 +18,7 @@
 
 %define         short_name openQA
 Name:           %{short_name}-test
-Version:        5.1769603414.6c0fa72e
+Version:        5.1769644379.ef069e9d
 Release:        0
 Summary:        Test package for openQA
 License:        GPL-2.0-or-later

++++++ openQA-worker-test.spec ++++++
--- /var/tmp/diff_new_pack.U2tDce/_old  2026-02-05 18:02:27.660794000 +0100
+++ /var/tmp/diff_new_pack.U2tDce/_new  2026-02-05 18:02:27.688795175 +0100
@@ -18,7 +18,7 @@
 
 %define         short_name openQA-worker
 Name:           %{short_name}-test
-Version:        5.1769603414.6c0fa72e
+Version:        5.1769644379.ef069e9d
 Release:        0
 Summary:        Test package for %{short_name}
 License:        GPL-2.0-or-later

++++++ openQA.spec ++++++
--- /var/tmp/diff_new_pack.U2tDce/_old  2026-02-05 18:02:28.104812626 +0100
+++ /var/tmp/diff_new_pack.U2tDce/_new  2026-02-05 18:02:28.136813969 +0100
@@ -59,10 +59,10 @@
 # The following line is generated from dependencies.yaml
 %define assetpack_requires perl(CSS::Minifier::XS) >= 0.01 
perl(JavaScript::Minifier::XS) >= 0.11 perl(Mojolicious) 
perl(Mojolicious::Plugin::AssetPack) >= 1.36 perl(YAML::PP) >= 0.026
 # The following line is generated from dependencies.yaml
-%define common_requires ntp-daemon perl >= 5.20.0 perl(Carp::Always) >= 
0.14.02 perl(Config::IniFiles) perl(Config::Tiny) perl(Cpanel::JSON::XS) >= 
4.09 perl(Cwd) perl(Data::Dump) perl(Data::Dumper) perl(Digest::MD5) 
perl(Feature::Compat::Try) perl(Filesys::Df) perl(Getopt::Long) perl(Minion) >= 
10.25 perl(Mojolicious) >= 9.340.0 perl(Regexp::Common) perl(Storable) 
perl(Text::Glob) perl(Time::Moment)
+%define common_requires ntp-daemon perl >= 5.20.0 perl(Carp::Always) >= 
0.14.02 perl(Config::IniFiles) perl(Cpanel::JSON::XS) >= 4.09 perl(Cwd) 
perl(Data::Dump) perl(Data::Dumper) perl(Digest::MD5) 
perl(Feature::Compat::Try) perl(Filesys::Df) perl(Getopt::Long) perl(Minion) >= 
10.25 perl(Mojolicious) >= 9.340.0 perl(Regexp::Common) perl(Storable) 
perl(Text::Glob) perl(Time::Moment)
 # runtime requirements for the main package that are not required by other 
sub-packages
 # The following line is generated from dependencies.yaml
-%define main_requires %assetpack_requires bsdtar git-core hostname 
openssh-clients perl(BSD::Resource) perl(Carp) perl(CommonMark) 
perl(Config::Tiny) perl(DBD::Pg) >= 3.7.4 perl(DBI) >= 1.632 perl(DBIx::Class) 
>= 0.082801 perl(DBIx::Class::DeploymentHandler) 
perl(DBIx::Class::DynamicDefault) perl(DBIx::Class::OptimisticLocking) 
perl(DBIx::Class::ResultClass::HashRefInflator) 
perl(DBIx::Class::Schema::Config) perl(DBIx::Class::Storage::Statistics) 
perl(Date::Format) perl(DateTime) perl(DateTime::Duration) 
perl(DateTime::Format::Pg) perl(Exporter) perl(Fcntl) perl(File::Basename) 
perl(File::Copy) perl(File::Copy::Recursive) perl(File::Path) perl(File::Spec) 
perl(FindBin) perl(Getopt::Long::Descriptive) perl(IO::Handle) perl(IPC::Run) 
perl(JSON::Validator) perl(LWP::UserAgent) perl(Module::Load::Conditional) 
perl(Module::Pluggable) perl(Mojo::Base) perl(Mojo::ByteStream) 
perl(Mojo::IOLoop) perl(Mojo::JSON) perl(Mojo::Pg) perl(Mojo::RabbitMQ::Client) 
>= 0.2 perl(Mojo::URL) perl(Mojo::Ut
 il) perl(Mojolicious::Commands) perl(Mojolicious::Plugin) 
perl(Mojolicious::Plugin::OAuth2) perl(Mojolicious::Static) 
perl(Net::OpenID::Consumer) perl(POSIX) perl(Pod::POM) perl(SQL::Translator) 
perl(Scalar::Util) perl(Sort::Versions) perl(Text::Diff) perl(Time::HiRes) 
perl(Time::ParseDate) perl(Time::Piece) perl(Time::Seconds) perl(URI::Escape) 
perl(YAML::PP) >= 0.026 perl(YAML::XS) perl(aliased) perl(base) perl(constant) 
perl(diagnostics) perl(strict) perl(warnings)
+%define main_requires %assetpack_requires bsdtar git-core hostname 
openssh-clients perl(BSD::Resource) perl(Carp) perl(CommonMark) perl(DBD::Pg) 
>= 3.7.4 perl(DBI) >= 1.632 perl(DBIx::Class) >= 0.082801 
perl(DBIx::Class::DeploymentHandler) perl(DBIx::Class::DynamicDefault) 
perl(DBIx::Class::OptimisticLocking) 
perl(DBIx::Class::ResultClass::HashRefInflator) 
perl(DBIx::Class::Schema::Config) perl(DBIx::Class::Storage::Statistics) 
perl(Date::Format) perl(DateTime) perl(DateTime::Duration) 
perl(DateTime::Format::Pg) perl(Exporter) perl(Fcntl) perl(File::Basename) 
perl(File::Copy) perl(File::Copy::Recursive) perl(File::Path) perl(File::Spec) 
perl(FindBin) perl(Getopt::Long::Descriptive) perl(IO::Handle) perl(IPC::Run) 
perl(JSON::Validator) perl(LWP::UserAgent) perl(Module::Load::Conditional) 
perl(Module::Pluggable) perl(Mojo::Base) perl(Mojo::ByteStream) 
perl(Mojo::IOLoop) perl(Mojo::JSON) perl(Mojo::Pg) perl(Mojo::RabbitMQ::Client) 
>= 0.2 perl(Mojo::URL) perl(Mojo::Util) perl(Mojoliciou
 s::Commands) perl(Mojolicious::Plugin) perl(Mojolicious::Plugin::OAuth2) 
perl(Mojolicious::Static) perl(Net::OpenID::Consumer) perl(POSIX) 
perl(Pod::POM) perl(SQL::Translator) perl(Scalar::Util) perl(Sort::Versions) 
perl(Text::Diff) perl(Time::HiRes) perl(Time::ParseDate) perl(Time::Piece) 
perl(Time::Seconds) perl(URI::Escape) perl(YAML::PP) >= 0.026 perl(YAML::XS) 
perl(aliased) perl(base) perl(constant) perl(diagnostics) perl(strict) 
perl(warnings)
 # The following line is generated from dependencies.yaml
 %define client_requires curl git-core jq perl(Getopt::Long::Descriptive) 
perl(IO::Socket::SSL) >= 2.009 perl(IPC::Run) perl(JSON::Validator) 
perl(LWP::Protocol::https) perl(LWP::UserAgent) perl(Test::More) perl(YAML::PP) 
>= 0.020 perl(YAML::XS)
 # The following line is generated from dependencies.yaml
@@ -83,7 +83,7 @@
 # Do not require on this in individual sub-packages except for the devel
 # package.
 # The following line is generated from dependencies.yaml
-%define test_requires %common_requires %main_requires %mcp_requires 
%python_scripts_requires %worker_requires curl jq openssh-common os-autoinst 
perl(App::cpanminus) perl(Selenium::Remote::Driver) >= 1.23 
perl(Selenium::Remote::WDKeys) perl(Test::Exception) perl(Test::Fatal) 
perl(Test::MockModule) perl(Test::MockObject) perl(Test::Mojo) perl(Test::Most) 
perl(Test::Output) perl(Test::Pod) perl(Test::Strict) perl(Test::Warnings) >= 
0.029 postgresql-server >= 14 python3-setuptools
+%define test_requires %common_requires %main_requires %mcp_requires 
%python_scripts_requires %worker_requires curl file jq openssh-common 
os-autoinst perl(App::cpanminus) perl(Selenium::Remote::Driver) >= 1.23 
perl(Selenium::Remote::WDKeys) perl(Test::Exception) perl(Test::Fatal) 
perl(Test::MockModule) perl(Test::MockObject) perl(Test::Mojo) perl(Test::Most) 
perl(Test::Output) perl(Test::Pod) perl(Test::Strict) perl(Test::Warnings) >= 
0.029 postgresql-server >= 14 python3-setuptools
 %ifarch x86_64
 %define qemu qemu qemu-kvm
 %else
@@ -99,7 +99,7 @@
 %define devel_requires %devel_no_selenium_requires chromedriver
 
 Name:           openQA
-Version:        5.1769603414.6c0fa72e
+Version:        5.1769644379.ef069e9d
 Release:        0
 Summary:        The openQA web-frontend, scheduler and tools
 License:        GPL-2.0-or-later

++++++ openQA-5.1769603414.6c0fa72e.obscpio -> 
openQA-5.1769644379.ef069e9d.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/openQA-5.1769603414.6c0fa72e/codecov.yml 
new/openQA-5.1769644379.ef069e9d/codecov.yml
--- old/openQA-5.1769603414.6c0fa72e/codecov.yml        2026-01-28 
13:30:14.000000000 +0100
+++ new/openQA-5.1769644379.ef069e9d/codecov.yml        2026-01-29 
00:52:59.000000000 +0100
@@ -26,6 +26,10 @@
           - lib/OpenQA/WebAPI/Controller/File.pm
           - lib/OpenQA/WebAPI/Controller/Step.pm
           - lib/OpenQA/WebAPI/Controller/Test.pm
+          - lib/OpenQA/WebAPI/Controller/API/V1/Iso.pm
+          - lib/OpenQA/WebAPI/Controller/API/V1/Job.pm
+          - lib/OpenQA/WebAPI/Controller/API/V1/JobGroup.pm
+          - lib/OpenQA/WebAPI/Controller/API/V1/Mm.pm
       tests:
         target: 100.0
         threshold: 0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/openQA-5.1769603414.6c0fa72e/cpanfile 
new/openQA-5.1769644379.ef069e9d/cpanfile
--- old/openQA-5.1769603414.6c0fa72e/cpanfile   2026-01-28 13:30:14.000000000 
+0100
+++ new/openQA-5.1769644379.ef069e9d/cpanfile   2026-01-29 00:52:59.000000000 
+0100
@@ -11,7 +11,6 @@
 requires 'Carp::Always', '>= 0.14.02';
 requires 'CommonMark';
 requires 'Config::IniFiles';
-requires 'Config::Tiny';
 requires 'Cpanel::JSON::XS', '>= 4.09';
 requires 'Cwd';
 requires 'DBD::Pg', '>= 3.007004';
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/openQA-5.1769603414.6c0fa72e/dependencies.yaml 
new/openQA-5.1769644379.ef069e9d/dependencies.yaml
--- old/openQA-5.1769603414.6c0fa72e/dependencies.yaml  2026-01-28 
13:30:14.000000000 +0100
+++ new/openQA-5.1769644379.ef069e9d/dependencies.yaml  2026-01-29 
00:52:59.000000000 +0100
@@ -51,7 +51,6 @@
   perl(Storable):
   perl(Text::Glob):
   perl(Time::Moment):
-  perl(Config::Tiny):
   ntp-daemon:
 
 cover_requires:
@@ -105,7 +104,6 @@
   perl(warnings):
   perl(BSD::Resource):
   perl(Carp):
-  perl(Config::Tiny):
   perl(CommonMark):
   perl(Date::Format):
   perl(DateTime):
@@ -190,6 +188,7 @@
   '%worker_requires':
   openssh-common:
   curl:
+  file:
   jq:
   os-autoinst:
   postgresql-server: '>= 14'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/openQA-5.1769603414.6c0fa72e/dist/rpm/openQA.spec 
new/openQA-5.1769644379.ef069e9d/dist/rpm/openQA.spec
--- old/openQA-5.1769603414.6c0fa72e/dist/rpm/openQA.spec       2026-01-28 
13:30:14.000000000 +0100
+++ new/openQA-5.1769644379.ef069e9d/dist/rpm/openQA.spec       2026-01-29 
00:52:59.000000000 +0100
@@ -59,10 +59,10 @@
 # The following line is generated from dependencies.yaml
 %define assetpack_requires perl(CSS::Minifier::XS) >= 0.01 
perl(JavaScript::Minifier::XS) >= 0.11 perl(Mojolicious) 
perl(Mojolicious::Plugin::AssetPack) >= 1.36 perl(YAML::PP) >= 0.026
 # The following line is generated from dependencies.yaml
-%define common_requires ntp-daemon perl >= 5.20.0 perl(Carp::Always) >= 
0.14.02 perl(Config::IniFiles) perl(Config::Tiny) perl(Cpanel::JSON::XS) >= 
4.09 perl(Cwd) perl(Data::Dump) perl(Data::Dumper) perl(Digest::MD5) 
perl(Feature::Compat::Try) perl(Filesys::Df) perl(Getopt::Long) perl(Minion) >= 
10.25 perl(Mojolicious) >= 9.340.0 perl(Regexp::Common) perl(Storable) 
perl(Text::Glob) perl(Time::Moment)
+%define common_requires ntp-daemon perl >= 5.20.0 perl(Carp::Always) >= 
0.14.02 perl(Config::IniFiles) perl(Cpanel::JSON::XS) >= 4.09 perl(Cwd) 
perl(Data::Dump) perl(Data::Dumper) perl(Digest::MD5) 
perl(Feature::Compat::Try) perl(Filesys::Df) perl(Getopt::Long) perl(Minion) >= 
10.25 perl(Mojolicious) >= 9.340.0 perl(Regexp::Common) perl(Storable) 
perl(Text::Glob) perl(Time::Moment)
 # runtime requirements for the main package that are not required by other 
sub-packages
 # The following line is generated from dependencies.yaml
-%define main_requires %assetpack_requires bsdtar git-core hostname 
openssh-clients perl(BSD::Resource) perl(Carp) perl(CommonMark) 
perl(Config::Tiny) perl(DBD::Pg) >= 3.7.4 perl(DBI) >= 1.632 perl(DBIx::Class) 
>= 0.082801 perl(DBIx::Class::DeploymentHandler) 
perl(DBIx::Class::DynamicDefault) perl(DBIx::Class::OptimisticLocking) 
perl(DBIx::Class::ResultClass::HashRefInflator) 
perl(DBIx::Class::Schema::Config) perl(DBIx::Class::Storage::Statistics) 
perl(Date::Format) perl(DateTime) perl(DateTime::Duration) 
perl(DateTime::Format::Pg) perl(Exporter) perl(Fcntl) perl(File::Basename) 
perl(File::Copy) perl(File::Copy::Recursive) perl(File::Path) perl(File::Spec) 
perl(FindBin) perl(Getopt::Long::Descriptive) perl(IO::Handle) perl(IPC::Run) 
perl(JSON::Validator) perl(LWP::UserAgent) perl(Module::Load::Conditional) 
perl(Module::Pluggable) perl(Mojo::Base) perl(Mojo::ByteStream) 
perl(Mojo::IOLoop) perl(Mojo::JSON) perl(Mojo::Pg) perl(Mojo::RabbitMQ::Client) 
>= 0.2 perl(Mojo::URL) perl(Mojo::Ut
 il) perl(Mojolicious::Commands) perl(Mojolicious::Plugin) 
perl(Mojolicious::Plugin::OAuth2) perl(Mojolicious::Static) 
perl(Net::OpenID::Consumer) perl(POSIX) perl(Pod::POM) perl(SQL::Translator) 
perl(Scalar::Util) perl(Sort::Versions) perl(Text::Diff) perl(Time::HiRes) 
perl(Time::ParseDate) perl(Time::Piece) perl(Time::Seconds) perl(URI::Escape) 
perl(YAML::PP) >= 0.026 perl(YAML::XS) perl(aliased) perl(base) perl(constant) 
perl(diagnostics) perl(strict) perl(warnings)
+%define main_requires %assetpack_requires bsdtar git-core hostname 
openssh-clients perl(BSD::Resource) perl(Carp) perl(CommonMark) perl(DBD::Pg) 
>= 3.7.4 perl(DBI) >= 1.632 perl(DBIx::Class) >= 0.082801 
perl(DBIx::Class::DeploymentHandler) perl(DBIx::Class::DynamicDefault) 
perl(DBIx::Class::OptimisticLocking) 
perl(DBIx::Class::ResultClass::HashRefInflator) 
perl(DBIx::Class::Schema::Config) perl(DBIx::Class::Storage::Statistics) 
perl(Date::Format) perl(DateTime) perl(DateTime::Duration) 
perl(DateTime::Format::Pg) perl(Exporter) perl(Fcntl) perl(File::Basename) 
perl(File::Copy) perl(File::Copy::Recursive) perl(File::Path) perl(File::Spec) 
perl(FindBin) perl(Getopt::Long::Descriptive) perl(IO::Handle) perl(IPC::Run) 
perl(JSON::Validator) perl(LWP::UserAgent) perl(Module::Load::Conditional) 
perl(Module::Pluggable) perl(Mojo::Base) perl(Mojo::ByteStream) 
perl(Mojo::IOLoop) perl(Mojo::JSON) perl(Mojo::Pg) perl(Mojo::RabbitMQ::Client) 
>= 0.2 perl(Mojo::URL) perl(Mojo::Util) perl(Mojoliciou
 s::Commands) perl(Mojolicious::Plugin) perl(Mojolicious::Plugin::OAuth2) 
perl(Mojolicious::Static) perl(Net::OpenID::Consumer) perl(POSIX) 
perl(Pod::POM) perl(SQL::Translator) perl(Scalar::Util) perl(Sort::Versions) 
perl(Text::Diff) perl(Time::HiRes) perl(Time::ParseDate) perl(Time::Piece) 
perl(Time::Seconds) perl(URI::Escape) perl(YAML::PP) >= 0.026 perl(YAML::XS) 
perl(aliased) perl(base) perl(constant) perl(diagnostics) perl(strict) 
perl(warnings)
 # The following line is generated from dependencies.yaml
 %define client_requires curl git-core jq perl(Getopt::Long::Descriptive) 
perl(IO::Socket::SSL) >= 2.009 perl(IPC::Run) perl(JSON::Validator) 
perl(LWP::Protocol::https) perl(LWP::UserAgent) perl(Test::More) perl(YAML::PP) 
>= 0.020 perl(YAML::XS)
 # The following line is generated from dependencies.yaml
@@ -83,7 +83,7 @@
 # Do not require on this in individual sub-packages except for the devel
 # package.
 # The following line is generated from dependencies.yaml
-%define test_requires %common_requires %main_requires %mcp_requires 
%python_scripts_requires %worker_requires curl jq openssh-common os-autoinst 
perl(App::cpanminus) perl(Selenium::Remote::Driver) >= 1.23 
perl(Selenium::Remote::WDKeys) perl(Test::Exception) perl(Test::Fatal) 
perl(Test::MockModule) perl(Test::MockObject) perl(Test::Mojo) perl(Test::Most) 
perl(Test::Output) perl(Test::Pod) perl(Test::Strict) perl(Test::Warnings) >= 
0.029 postgresql-server >= 14 python3-setuptools
+%define test_requires %common_requires %main_requires %mcp_requires 
%python_scripts_requires %worker_requires curl file jq openssh-common 
os-autoinst perl(App::cpanminus) perl(Selenium::Remote::Driver) >= 1.23 
perl(Selenium::Remote::WDKeys) perl(Test::Exception) perl(Test::Fatal) 
perl(Test::MockModule) perl(Test::MockObject) perl(Test::Mojo) perl(Test::Most) 
perl(Test::Output) perl(Test::Pod) perl(Test::Strict) perl(Test::Warnings) >= 
0.029 postgresql-server >= 14 python3-setuptools
 %ifarch x86_64
 %define qemu qemu qemu-kvm
 %else
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/openQA-5.1769603414.6c0fa72e/lib/OpenQA/WebAPI/Controller/API/V1/Iso.pm 
new/openQA-5.1769644379.ef069e9d/lib/OpenQA/WebAPI/Controller/API/V1/Iso.pm
--- old/openQA-5.1769603414.6c0fa72e/lib/OpenQA/WebAPI/Controller/API/V1/Iso.pm 
2026-01-28 13:30:14.000000000 +0100
+++ new/openQA-5.1769644379.ef069e9d/lib/OpenQA/WebAPI/Controller/API/V1/Iso.pm 
2026-01-29 00:52:59.000000000 +0100
@@ -38,21 +38,12 @@
 
 =cut
 
-sub show_scheduled_product {
-    my ($self) = @_;
-
+sub show_scheduled_product ($self) {
     my $scheduled_product_id = $self->param('scheduled_product_id');
     my $scheduled_products = 
$self->app->schema->resultset('ScheduledProducts');
-    my $scheduled_product = $scheduled_products->find($scheduled_product_id);
-    if (!$scheduled_product) {
-        return $self->render(
-            json => {error => 'Scheduled product does not exist.'},
-            status => 404,
-        );
-    }
-
+    return $self->render(json => {error => 'Scheduled product does not 
exist.'}, status => 404)
+      unless my $scheduled_product = 
$scheduled_products->find($scheduled_product_id);
     my @args = (include_job_ids => $self->param('include_job_ids'));
-
     $self->render(json => $scheduled_product->to_hash(@args));
 }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/openQA-5.1769603414.6c0fa72e/lib/OpenQA/WebAPI/Controller/API/V1/JobGroup.pm
 
new/openQA-5.1769644379.ef069e9d/lib/OpenQA/WebAPI/Controller/API/V1/JobGroup.pm
--- 
old/openQA-5.1769603414.6c0fa72e/lib/OpenQA/WebAPI/Controller/API/V1/JobGroup.pm
    2026-01-28 13:30:14.000000000 +0100
+++ 
new/openQA-5.1769644379.ef069e9d/lib/OpenQA/WebAPI/Controller/API/V1/JobGroup.pm
    2026-01-29 00:52:59.000000000 +0100
@@ -38,9 +38,7 @@
 
 =cut
 
-sub is_parent ($self) {
-    return $self->req->url->path =~ qr/.*\/parent_groups/;
-}
+sub is_parent ($self) { $self->req->url->path =~ qr/.*\/parent_groups/ }
 
 =over 4
 
@@ -52,9 +50,7 @@
 
 =cut
 
-sub resultset ($self) {
-    return $self->schema->resultset($self->is_parent ? 'JobGroupParents' : 
'JobGroups');
-}
+sub resultset ($self) { $self->schema->resultset($self->is_parent ? 
'JobGroupParents' : 'JobGroups') }
 
 =over 4
 
@@ -67,18 +63,10 @@
 =cut
 
 sub find_group ($self) {
-    my $group_id = $self->param('group_id');
-    if (!defined $group_id) {
-        $self->render(json => {error => 'No group ID specified'}, status => 
400);
-        return;
-    }
-
-    my $group = $self->resultset->find($group_id);
-    if (!$group) {
-        $self->render(json => {error => "Job group $group_id does not exist"}, 
status => 404);
-        return;
-    }
-
+    return $self->render(json => {error => 'No group ID specified'}, status => 
400) && 0
+      unless defined(my $group_id = $self->param('group_id'));
+    return $self->render(json => {error => "Job group $group_id does not 
exist"}, status => 404) && 0
+      unless my $group = $self->resultset->find($group_id);
     return $group;
 }
 
@@ -297,8 +285,7 @@
 =cut
 
 sub update ($self) {
-    my $group = $self->find_group;
-    return unless $group;
+    return unless my $group = $self->find_group;
 
     my $validation = $self->validation;
     # Don't check group name if sorting group by dragging
@@ -336,8 +323,7 @@
 =cut
 
 sub list_jobs ($self) {
-    my $group = $self->find_group;
-    return unless $group;
+    return unless my $group = $self->find_group;
 
     my @jobs;
     if ($self->param('expired')) {
@@ -361,8 +347,7 @@
 =cut
 
 sub delete ($self) {
-    my $group = $self->find_group();
-    return unless $group;
+    return unless my $group = $self->find_group;
 
     if ($group->can('jobs') && scalar($group->jobs) != 0) {
         return $self->render(json => {error => 'Job group ' . $group->id . ' 
is not empty'}, status => 400);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/openQA-5.1769603414.6c0fa72e/lib/OpenQA/WebAPI/Controller/API/V1/Mm.pm 
new/openQA-5.1769644379.ef069e9d/lib/OpenQA/WebAPI/Controller/API/V1/Mm.pm
--- old/openQA-5.1769603414.6c0fa72e/lib/OpenQA/WebAPI/Controller/API/V1/Mm.pm  
2026-01-28 13:30:14.000000000 +0100
+++ new/openQA-5.1769644379.ef069e9d/lib/OpenQA/WebAPI/Controller/API/V1/Mm.pm  
2026-01-29 00:52:59.000000000 +0100
@@ -1,10 +1,11 @@
-# Copyright 2015 SUSE LLC
+# Copyright SUSE LLC
 # SPDX-License-Identifier: GPL-2.0-or-later
 
 package OpenQA::WebAPI::Controller::API::V1::Mm;
-use Mojo::Base 'Mojolicious::Controller';
+use Mojo::Base 'Mojolicious::Controller', -signatures;
 
-use OpenQA::Jobs::Constants;
+use OpenQA::Jobs::Constants qw(RUNNING SCHEDULED DONE);
+use OpenQA::JobDependencies::Constants qw(PARALLEL);
 use OpenQA::Schema::Result::Jobs;
 use OpenQA::Schema::Result::JobDependencies;
 
@@ -38,24 +39,22 @@
 
 # this needs 2 calls to do anything useful
 # IMHO it should be replaced with get_children and removed
-sub get_children_status {
-    my ($self) = @_;
-    my $state = $self->stash('state');
-    if ($state eq 'running') {
-        $state = OpenQA::Jobs::Constants::RUNNING;
-    }
-    elsif ($state eq 'scheduled') {
-        $state = OpenQA::Jobs::Constants::SCHEDULED;
-    }
-    else {
-        $state = OpenQA::Jobs::Constants::DONE;
-    }
+
+my %STATE_MAPPING = (running => RUNNING, scheduled => SCHEDULED);
+
+sub get_children_status ($self) {
+    my $state = $STATE_MAPPING{$self->stash('state')} // DONE;
     my $jobid = $self->stash('job_id');
+    my $jobs = $self->schema->resultset('Jobs');
+    my %attr = (columns => ['id'], join => 'parents');
+    my @res = $jobs->search({'parents.parent_job_id' => $jobid, state => 
$state}, \%attr);
+    return $self->render(json => {jobs => [map { $_->id } @res]}, status => 
200);
+}
 
-    my @res = $self->schema->resultset('Jobs')
-      ->search({'parents.parent_job_id' => $jobid, state => $state}, {columns 
=> ['id'], join => 'parents'});
-    my @res_ids = map { $_->id } @res;
-    return $self->render(json => {jobs => \@res_ids}, status => 200);
+sub _find_jobs ($self, $id_column, $dependency_column, $columns, $join) {
+    my $job_id = $self->stash('job_id');
+    my $jobs = $self->schema->resultset('Jobs');
+    [$jobs->search({$id_column => $job_id, $dependency_column => PARALLEL}, 
{columns => $columns, join => $join})->all];
 }
 
 =over 4
@@ -69,17 +68,9 @@
 
 =cut
 
-sub get_children {
-    my ($self) = @_;
-    my $jobid = $self->stash('job_id');
-
-    my @res
-      = $self->schema->resultset('Jobs')
-      ->search(
-        {'parents.parent_job_id' => $jobid, 'parents.dependency' => 
OpenQA::JobDependencies::Constants::PARALLEL},
-        {columns => ['id', 'state'], join => 'parents'});
-    my %res_ids = map { ($_->id, $_->state) } @res;
-    return $self->render(json => {jobs => \%res_ids}, status => 200);
+sub get_children ($self) {
+    my $jobs = $self->_find_jobs('parents.parent_job_id', 
'parents.dependency', ['id', 'state'], 'parents');
+    $self->render(json => {jobs => {map { ($_->id, $_->state) } @$jobs}}, 
status => 200);
 }
 
 =over 4
@@ -93,17 +84,9 @@
 
 =cut
 
-sub get_parents {
-    my ($self) = @_;
-    my $jobid = $self->stash('job_id');
-
-    my @res
-      = $self->schema->resultset('Jobs')
-      ->search(
-        {'children.child_job_id' => $jobid, 'children.dependency' => 
OpenQA::JobDependencies::Constants::PARALLEL},
-        {columns => ['id'], join => 'children'});
-    my @res_ids = map { $_->id } @res;
-    return $self->render(json => {jobs => \@res_ids}, status => 200);
+sub get_parents ($self) {
+    my $jobs = $self->_find_jobs('children.child_job_id', 
'children.dependency', ['id'], 'children');
+    return $self->render(json => {jobs => [map { $_->id } @$jobs]}, status => 
200);
 }
 
 1;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/openQA-5.1769603414.6c0fa72e/lib/OpenQA/WebAPI/Controller/API/V1/User.pm 
new/openQA-5.1769644379.ef069e9d/lib/OpenQA/WebAPI/Controller/API/V1/User.pm
--- 
old/openQA-5.1769603414.6c0fa72e/lib/OpenQA/WebAPI/Controller/API/V1/User.pm    
    2026-01-28 13:30:14.000000000 +0100
+++ 
new/openQA-5.1769644379.ef069e9d/lib/OpenQA/WebAPI/Controller/API/V1/User.pm    
    2026-01-29 00:52:59.000000000 +0100
@@ -2,7 +2,9 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 
 package OpenQA::WebAPI::Controller::API::V1::User;
-use Mojo::Base 'Mojolicious::Controller';
+use Mojo::Base 'Mojolicious::Controller', -signatures;
+use DateTime::Format::Pg;
+use Feature::Compat::Try;
 
 =pod
 
@@ -30,8 +32,7 @@
 
 =cut
 
-sub delete {
-    my ($self) = @_;
+sub delete ($self) {
     my $user = $self->schema->resultset('Users')->find($self->param('id'));
     return $self->render(json => {error => 'Not found'}, status => 404) unless 
$user;
     my $result = $user->delete();
@@ -39,4 +40,72 @@
     $self->render(json => {result => $result});
 }
 
+=over 4
+
+=item create_api_key()
+
+Create a new API key.
+
+=back
+
+=cut
+
+sub create_api_key ($self) {
+    my $user = $self->current_user;
+    my $expiration;
+    my $validation = $self->validation;
+    $validation->optional('expiration', 'seconds_optional')->datetime;
+    return $self->render(
+        json => {error => 'Date must be in format ' . 
DateTime::Format::Pg->format_datetime(DateTime->now())},
+        status => 400
+    ) if $validation->has_error;
+    $expiration
+      = $validation->is_valid('expiration')
+      ? DateTime::Format::Pg->parse_datetime($validation->param('expiration'))
+      : DateTime->now->add(years => 1);
+    my $apikey = $user->api_keys->create({t_expiration => $expiration});
+    $self->render(json => {id => $apikey->id, key => $apikey->key, 
t_expiration => $apikey->t_expiration});
+}
+
+=over 4
+
+=item list_api_keys()
+
+List API keys of the current user.
+
+=back
+
+=cut
+
+sub list_api_keys ($self) {
+    my $user = $self->current_user;
+    my @keys = map {
+        {
+            key => $_->key,
+            t_expiration => $_->t_expiration,
+            t_created => $_->t_created,
+            t_updated => $_->t_updated,
+        }
+    } $user->api_keys->all;
+    $self->render(json => {keys => \@keys});
+}
+
+=over 4
+
+=item delete_api_key()
+
+Delete a specific API key of the current user.
+
+=back
+
+=cut
+
+sub delete_api_key ($self) {
+    my $user = $self->current_user;
+    my $key = $user->api_keys->find({key => $self->param('key')});
+    return $self->render(json => {error => 'Not found'}, status => 404) unless 
$key;
+    $key->delete;
+    $self->render(json => {result => 1});
+}
+
 1;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/openQA-5.1769603414.6c0fa72e/lib/OpenQA/WebAPI.pm 
new/openQA-5.1769644379.ef069e9d/lib/OpenQA/WebAPI.pm
--- old/openQA-5.1769603414.6c0fa72e/lib/OpenQA/WebAPI.pm       2026-01-28 
13:30:14.000000000 +0100
+++ new/openQA-5.1769644379.ef069e9d/lib/OpenQA/WebAPI.pm       2026-01-29 
00:52:59.000000000 +0100
@@ -469,6 +469,10 @@
 
     
$api_ra->delete('/user/<id:num>')->name('apiv1_delete_user')->to('user#delete');
 
+    
$api_ru->get('/users/me/api_keys')->name('apiv1_list_user_api_keys')->to('user#list_api_keys');
+    
$api_ru->post('/users/me/api_keys')->name('apiv1_create_user_api_key')->to('user#create_api_key');
+    
$api_ru->delete('/users/me/api_keys/<key>')->name('apiv1_delete_user_api_key')->to('user#delete_api_key');
+
     # api/v1/search
     
$api_public_r->get('/experimental/search')->name('apiv1_search_query')->to('search#query');
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/openQA-5.1769603414.6c0fa72e/script/openqa-bootstrap 
new/openQA-5.1769644379.ef069e9d/script/openqa-bootstrap
--- old/openQA-5.1769603414.6c0fa72e/script/openqa-bootstrap    2026-01-28 
13:30:14.000000000 +0100
+++ new/openQA-5.1769644379.ef069e9d/script/openqa-bootstrap    2026-01-29 
00:52:59.000000000 +0100
@@ -112,8 +112,8 @@
 proxy_args=""
 [[ -n "$setup_web_proxy" ]] && proxy_args="--proxy=$setup_web_proxy"
 setup=$OPENQA_DIR/script/configure-web-proxy
-if command -v $setup; then
-    bash -ex $setup "$proxy_args"
+if command -v "$setup"; then
+    bash -ex "$setup" "$proxy_args"
 else
     curl -s 
https://raw.githubusercontent.com/os-autoinst/openQA/master/script/configure-web-proxy
 | bash -ex -s -- "$proxy_args"
 fi
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/openQA-5.1769603414.6c0fa72e/t/05-scheduler-dependencies.t 
new/openQA-5.1769644379.ef069e9d/t/05-scheduler-dependencies.t
--- old/openQA-5.1769603414.6c0fa72e/t/05-scheduler-dependencies.t      
2026-01-28 13:30:14.000000000 +0100
+++ new/openQA-5.1769644379.ef069e9d/t/05-scheduler-dependencies.t      
2026-01-29 00:52:59.000000000 +0100
@@ -345,11 +345,17 @@
 
 sub _check_mm_api {
     my $explain_tx_res = sub {
-        always_explain $t->tx->res->content;    # uncoverable statement
+        always_explain $t->tx->res->body;    # uncoverable statement
     };
     $t->get_ok('/api/v1/mm/children/running')->status_is(200)->json_is('/jobs' 
=> [$jobF->id])->or($explain_tx_res);
     
$t->get_ok('/api/v1/mm/children/scheduled')->status_is(200)->json_is('/jobs' => 
[])->or($explain_tx_res);
     $t->get_ok('/api/v1/mm/children/done')->status_is(200)->json_is('/jobs' => 
[$jobE->id])->or($explain_tx_res);
+
+    my %children = ($jobF->id => RUNNING, $jobE->id => DONE);
+    $t->get_ok('/api/v1/mm/children')->status_is(200)->json_is('/jobs' => 
\%children)->or($explain_tx_res);
+
+    my @parents = (99983);
+    $t->get_ok('/api/v1/mm/parents')->status_is(200)->json_is('/jobs' => 
\@parents)->or($explain_tx_res);
 }
 subtest 'MM API for children status - available only for running jobs' => sub {
     isnt(my $job_token = 
$sent->{job}->{$jobC->id}->{worker}->get_property('JOBTOKEN'), undef, 'JOBTOKEN 
is present');
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/openQA-5.1769603414.6c0fa72e/t/api/02-iso.t 
new/openQA-5.1769644379.ef069e9d/t/api/02-iso.t
--- old/openQA-5.1769603414.6c0fa72e/t/api/02-iso.t     2026-01-28 
13:30:14.000000000 +0100
+++ new/openQA-5.1769644379.ef069e9d/t/api/02-iso.t     2026-01-29 
00:52:59.000000000 +0100
@@ -85,6 +85,11 @@
     group_id => 1002,
 );
 
+subtest 'querying non-existent scheduled product' => sub {
+    $t->get_ok('/api/v1/isos/999')->status_is(404);
+    $t->json_is('/error' => 'Scheduled product does not exist.');
+};
+
 subtest 'group filter and priority override' => sub {
     # add a job template for group 1002
     my $job_template = $job_templates->create({@job_template_params, 
product_id => 1});
@@ -824,7 +829,12 @@
 subtest 're-schedule product' => sub {
     plan skip_all => 'previous test "async flag" has not scheduled a product' 
unless $scheduled_product_id;
 
-    my $res = schedule_iso($t, {scheduled_product_clone_id => 
$scheduled_product_id}, 200, {async => 1});
+    my $res = schedule_iso($t, {scheduled_product_clone_id => 'foobar'}, 400);
+    like $res->body, qr/scheduled_product_id.*invalid/, 'error returned if 
scheduled product to clone from is invalid';
+    $res = schedule_iso($t, {scheduled_product_clone_id => 1234567}, 404);
+    like $res->body, qr/to clone.*not found/, 'error returned if scheduled 
product to clone from does not exist';
+
+    $res = schedule_iso($t, {scheduled_product_clone_id => 
$scheduled_product_id}, 200, {async => 1});
     my $json = $res->json;
     my $cloned_scheduled_product_id = $json->{scheduled_product_id};
     ok($cloned_scheduled_product_id, 'scheduled product ID returned');
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/openQA-5.1769603414.6c0fa72e/t/api/04-jobs.t 
new/openQA-5.1769644379.ef069e9d/t/api/04-jobs.t
--- old/openQA-5.1769603414.6c0fa72e/t/api/04-jobs.t    2026-01-28 
13:30:14.000000000 +0100
+++ new/openQA-5.1769644379.ef069e9d/t/api/04-jobs.t    2026-01-29 
00:52:59.000000000 +0100
@@ -70,6 +70,7 @@
 my $schema = $t->app->schema;
 my $assets = $schema->resultset('Assets');
 my $jobs = $schema->resultset('Jobs');
+my $workers = $schema->resultset('Workers');
 my $comments = $schema->resultset('Comments');
 my $products = $schema->resultset('Products');
 my $testsuites = $schema->resultset('TestSuites');
@@ -468,11 +469,16 @@
 };
 
 subtest 'parameter validation on artefact upload' => sub {
-    
$t->post_ok('/api/v1/jobs/99963/artefact?file=not-a-file&md5=not-an-md5sum&image=1')->status_is(400)->json_is(
-        {
-            error_status => 400,
-            error => 'Erroneous parameters (file invalid, md5 invalid)',
-        });
+    
$t->post_ok('/api/v1/jobs/99963/artefact?file=not-a-file&md5=not-an-md5sum&image=1')->status_is(400);
+    $t->json_is({error_status => 400, error => 'Erroneous parameters (file 
invalid, md5 invalid)'});
+
+    combined_like { $t->post_ok('/api/v1/jobs/34563/artefact')->status_is(404) 
} qr/artefact for non-existing job/,
+      'upload without job logged';
+    $t->json_is({error_status => 404, error => 'Specified job 34563 does not 
exist'});
+
+    combined_like { $t->post_ok('/api/v1/jobs/99926/artefact')->status_is(404) 
}
+    qr/artefact for job with no worker assigned/, 'upload without worker 
logged';
+    $t->json_is({error_status => 404, error => 'No worker assigned'});
 };
 
 my $expected_result_size = 0;
@@ -791,6 +797,26 @@
         # note: The arrays are supposed to be sorted so it is fine to assume a 
fix order here.
     };
 
+    $schema->txn_begin;
+
+    subtest 'update running job using assigned worker' => sub {
+        my $job = $jobs->search({id => 99963});
+        my $worker = $workers->search({job_id => 99963});
+
+        $job->update({state => RUNNING, result => NONE, assigned_worker_id => 
2, t_finished => undef});
+        $worker->update({job_id => undef});
+        $t->post_ok('/api/v1/jobs/99963/status', json => {status => {worker_id 
=> 2}})->status_is(200);
+        is $worker->count, 1, 'job of assigned worker updated';
+
+        $job->update({state => RUNNING, result => NONE, assigned_worker_id => 
undef});
+        $worker->update({job_id => undef});
+        $t->post_ok('/api/v1/jobs/99963/status', json => {status => {worker_id 
=> 2}})->status_is(400);
+        $t->json_like('/error' => qr/worker 2.*not.*assigned/, 'error about 
update without assigned worker');
+        is $worker->count, 0, 'job of worker not updated';
+    };
+
+    $schema->txn_rollback;
+
     subtest 'wrong parameters' => sub {
         combined_like {
             $t->post_ok('/api/v1/jobs/9999999/status', json => 
{})->status_is(400)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/openQA-5.1769603414.6c0fa72e/t/api/15-users.t 
new/openQA-5.1769644379.ef069e9d/t/api/15-users.t
--- old/openQA-5.1769603414.6c0fa72e/t/api/15-users.t   2026-01-28 
13:30:14.000000000 +0100
+++ new/openQA-5.1769644379.ef069e9d/t/api/15-users.t   2026-01-29 
00:52:59.000000000 +0100
@@ -5,6 +5,8 @@
 use Test::Most;
 use Test::Mojo;
 use Test::Warnings ':report_warnings';
+use Date::Format 'time2str';
+use Time::Seconds;
 use FindBin;
 use lib "$FindBin::Bin/../lib", 
"$FindBin::Bin/../../external/os-autoinst-common/lib";
 use OpenQA::Test::TimeLimit '8';
@@ -34,4 +36,49 @@
 $t->post_ok('/api/v1/feature?version=42')->status_is(200, 'can set the feature 
version of current user');
 is $app->schema->resultset('Users')->find(99902)->feature_version, 42, 
'feature version was updated';
 
+my $res = $t->post_ok('/api/v1/users/me/api_keys')->status_is(200, 'create api 
key')->tx->res->json;
+ok $res->{key}, 'key returned';
+my $expected_year = time2str('%Y', time + ONE_YEAR, 'UTC');
+like $res->{t_expiration}, qr/^$expected_year-/, 'default expiration is set to 
1 year from now';
+my $key1 = $res->{key};
+
+my $new_key = $app->schema->resultset('ApiKeys')->find({key => $res->{key}});
+ok $new_key, 'key found in DB';
+is $new_key->user_id, 99902, 'key belongs to correct user';
+
+subtest 'create_api_key with expiration' => sub {
+    my $expiration_time = time + ONE_YEAR;
+    my $expiration = time2str('%Y-%m-%d %H:%M:%S', $expiration_time, 'UTC');
+    $res = $t->post_ok('/api/v1/users/me/api_keys' => form => {expiration => 
$expiration})
+      ->status_is(200, 'create api key with expiration')->tx->res->json;
+    ok $res->{key}, 'key returned';
+    my $expected_year = time2str('%Y', $expiration_time, 'UTC');
+    like $res->{t_expiration}, qr/^$expected_year-/, 'expiration matches 
expected year';
+};
+
+subtest 'create_api_key with invalid expiration' => sub {
+    $t->post_ok('/api/v1/users/me/api_keys' => form => {expiration => 
'invalid'})
+      ->status_is(400, 'invalid expiration rejected');
+};
+
+subtest 'test list_api_keys' => sub {
+    my $key2 = $res->{key};
+    $res = $t->get_ok('/api/v1/users/me/api_keys')->status_is(200, 'list api 
keys')->tx->res->json;
+    my $api_keys = $res->{keys};
+    ok scalar @$api_keys >= 2, 'at least two keys found';
+    ok((grep { $_->{key} eq $key1 } @$api_keys), "key $key1 found in list");
+    ok((grep { $_->{key} eq $key2 } @$api_keys), "key $key2 found in list");
+    ok !exists $api_keys->[0]{secret}, 'secret is not returned in list';
+};
+
+subtest 'test delete_api_key' => sub {
+    my $count_before = scalar @{$res->{keys}};
+    $t->delete_ok("/api/v1/users/me/api_keys/$key1")->status_is(200, 'delete 
api key');
+    $res = 
$t->get_ok('/api/v1/users/me/api_keys')->status_is(200)->tx->res->json;
+    is scalar @{$res->{keys}}, $count_before - 1, 'one key less';
+    ok !((grep { $_->{key} eq $key1 } @{$res->{keys}})), "key $key1 no longer 
in list";
+
+    $t->delete_ok("/api/v1/users/me/api_keys/NONEXISTENT")->status_is(404, 
'delete nonexistent key');
+};
+
 done_testing();

++++++ openQA.obsinfo ++++++
--- /var/tmp/diff_new_pack.U2tDce/_old  2026-02-05 18:03:03.606301855 +0100
+++ /var/tmp/diff_new_pack.U2tDce/_new  2026-02-05 18:03:03.618302358 +0100
@@ -1,5 +1,5 @@
 name: openQA
-version: 5.1769603414.6c0fa72e
-mtime: 1769603414
-commit: 6c0fa72ec7b1b63bbc5faec28aaf246adb5acd8e
+version: 5.1769644379.ef069e9d
+mtime: 1769644379
+commit: ef069e9dffeab3a6dd3a1834218ed539ef3d6d82
 

Reply via email to