From 374ffa8a071146ece2fb38a6dad1fcc9430c5a67 Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Sun, 22 Feb 2026 20:18:43 +0000 Subject: [PATCH] Bug 39224: Unit tests for identity provider object classes Adds unit tests for the new Koha object classes: - t/db_dependent/Koha/Auth/Hostname.t: Tests for hostname CRUD and validation - t/db_dependent/Koha/Auth/Identity/Provider.t: Extended tests for provider with mappings and hostnames - t/db_dependent/Koha/Auth/Identity/Provider/Mapping.t: Tests for field mapping CRUD and validation - t/db_dependent/Auth_with_shibboleth.t: Updated tests for new provider structure Test plan: 1. Run: prove t/db_dependent/Koha/Auth/Hostname.t 2. Run: prove t/db_dependent/Koha/Auth/Identity/Provider.t 3. Run: prove t/db_dependent/Koha/Auth/Identity/Provider/Mapping.t 4. Run: prove t/db_dependent/Auth_with_shibboleth.t Sponsored-by: ByWater Solutions --- t/db_dependent/Auth_with_shibboleth.t | 83 ++++++- t/db_dependent/Koha/Auth/Hostname.t | 139 ++++++++++++ t/db_dependent/Koha/Auth/Identity/Provider.t | 158 +++++++++---- .../Koha/Auth/Identity/Provider/Mapping.t | 207 ++++++++++++++++++ 4 files changed, 537 insertions(+), 50 deletions(-) create mode 100755 t/db_dependent/Koha/Auth/Hostname.t create mode 100755 t/db_dependent/Koha/Auth/Identity/Provider/Mapping.t diff --git a/t/db_dependent/Auth_with_shibboleth.t b/t/db_dependent/Auth_with_shibboleth.t index 8a101ac07e6..ffa336b9991 100755 --- a/t/db_dependent/Auth_with_shibboleth.t +++ b/t/db_dependent/Auth_with_shibboleth.t @@ -35,6 +35,13 @@ use t::lib::Dates; use C4::Auth_with_shibboleth qw( shib_ok login_shib_url get_login_shib checkpw_shib ); use Koha::Database; use Koha::DateUtils qw( dt_from_string ); +use Koha::Auth::Hostname; +use Koha::Auth::Hostnames; +use Koha::Auth::Identity::Provider; +use Koha::Auth::Identity::Provider::Hostname; +use Koha::Auth::Identity::Provider::Mapping; +use Koha::Auth::Identity::Provider::SAML2; +use Koha::Auth::Identity::Providers; BEGIN { use_ok( @@ -48,9 +55,7 @@ $schema->storage->txn_begin; my $builder = t::lib::TestBuilder->new; my $logger = t::lib::Mocks::Logger->new(); -# Mock variables my $shibboleth; -change_config( {} ); my $interface = 'opac'; # Mock few preferences @@ -59,12 +64,13 @@ t::lib::Mocks::mock_preference( 'StaffClientBaseURL', 'teststaff.com' ); t::lib::Mocks::mock_preference( 'EmailFieldPrimary', '' ); t::lib::Mocks::mock_preference( 'EmailFieldPrecedence', 'emailpro' ); -# Mock Context: config, tz and interface +# Mock Context: tz and interface my $context = Test::MockModule->new('C4::Context'); -$context->mock( 'config', sub { return $shibboleth; } ); # easier than caching by Mocks::mock_config $context->mock( 'timezone', sub { return 'local'; } ); $context->mock( 'interface', sub { return $interface; } ); +change_config( {} ); + # Mock Letters: GetPreparedLetter, EnqueueLetter and SendQueuedMessages # We want to test the params my $mocked_letters = Test::MockModule->new('C4::Letters'); @@ -97,20 +103,22 @@ $mocked_letters->mock( # Start testing ---------------------------------------------------------------- subtest "shib_ok tests" => sub { - plan tests => 5; + plan tests => 6; my $result; # correct config, no debug is( shib_ok(), '1', "good config" ); - # bad config, no debug + # bad config, no debug - clear matchpoint in DB + $schema->resultset('IdentityProviderMapping')->update( { is_matchpoint => 0 } ); delete $shibboleth->{matchpoint}; + $logger->clear(); warnings_are { $result = shib_ok() } - [ { carped => 'shibboleth matchpoint not defined' }, ], - "undefined matchpoint = fatal config, warning given"; + [ { carped => 'shibboleth matchpoint not defined' } ], + "matchpoint warning given when no matchpoint set"; is( $result, '0', "bad config" ); - change_config( { matchpoint => 'email' } ); + change_config( { matchpoint => 'email', mapping => { email => { content => 'default@example.com' } } } ); warnings_are { $result = shib_ok() } [ { carped => 'shibboleth matchpoint not mapped' }, ], "unmapped matchpoint = fatal config, warning given"; @@ -119,6 +127,7 @@ subtest "shib_ok tests" => sub { # add test for undefined shibboleth block $logger->clear; change_config( {} ); + is( shib_ok(), '1', 'good config after reset' ); }; subtest "login_shib_url tests" => sub { @@ -279,6 +288,10 @@ subtest "checkpw_shib tests" => sub { ); # sync user + my $saml2_provider = Koha::Auth::Identity::Providers->search( { protocol => 'SAML2' } )->next; + my $saml2_config = $saml2_provider->get_config // {}; + $saml2_config->{sync} = 1; + $saml2_provider->set_config($saml2_config)->store; $shibboleth->{sync} = 1; $ENV{'city'} = 'AnotherCity'; ( $retval, $retcard, $retuserid, $retpatron ) = checkpw_shib($shib_login); @@ -335,7 +348,7 @@ subtest "checkpw_shib tests" => sub { $ENV{'emailpro'} = 'me@myemail.com'; $ENV{branchcode} = $library->branchcode; # needed since T::D::C does no longer hides the FK constraint - checkpw($shib_login); + ( $retval, $retcard, $retuserid, $retpatron ) = checkpw_shib($shib_login); ok( my $new_user_autocreated = Koha::Patrons->find( { userid => 'test43210' } ), "new user found" @@ -424,6 +437,38 @@ $schema->storage->txn_rollback; sub change_config { my $params = shift; + # Remove any existing SAML2 providers + Koha::Auth::Identity::Providers->search( { protocol => 'SAML2' } )->delete; + + my $provider = Koha::Auth::Identity::Provider::SAML2->new( + { + code => 'shibboleth', + description => 'Shibboleth', + enabled => 1, + } + )->store; + + $provider->set_config( + { + autocreate => $params->{autocreate} // 0, + sync => $params->{sync} // 0, + welcome => $params->{welcome} // 0, + } + )->store; + + # Link provider to the test hostname so _get_shib_config can find it + my $test_hostname = 'testopac.com'; + $ENV{SERVER_NAME} = $test_hostname; + my $hostname_obj = Koha::Auth::Hostnames->search( { hostname => $test_hostname } )->next + // $builder->build_object( { class => 'Koha::Auth::Hostnames', value => { hostname => $test_hostname } } ); + Koha::Auth::Identity::Provider::Hostname->new( + { + hostname_id => $hostname_obj->hostname_id, + identity_provider_id => $provider->identity_provider_id, + is_enabled => 1, + } + )->store; + my %mapping = ( 'userid' => { 'is' => 'uid' }, 'surname' => { 'is' => 'sn' }, @@ -437,15 +482,29 @@ sub change_config { if ( exists $params->{mapping} ) { $mapping{$_} = $params->{mapping}->{$_} for keys %{ $params->{mapping} }; } + + my $matchpoint = $params->{matchpoint} // 'userid'; + + foreach my $koha_field ( keys %mapping ) { + Koha::Auth::Identity::Provider::Mapping->new( + { + identity_provider_id => $provider->identity_provider_id, + koha_field => $koha_field, + provider_field => $mapping{$koha_field}->{is}, + is_matchpoint => $koha_field eq $matchpoint ? 1 : 0, + default_content => $mapping{$koha_field}->{content}, + } + )->store; + } + $shibboleth = { autocreate => $params->{autocreate} // 0, welcome => $params->{welcome} // 0, sync => $params->{sync} // 0, - matchpoint => $params->{matchpoint} // 'userid', + matchpoint => $matchpoint, mapping => \%mapping, }; - # Change environment too $ENV{'uid'} = "test1234"; $ENV{'sn'} = undef; $ENV{'exp'} = undef; diff --git a/t/db_dependent/Koha/Auth/Hostname.t b/t/db_dependent/Koha/Auth/Hostname.t new file mode 100755 index 00000000000..c66f993408b --- /dev/null +++ b/t/db_dependent/Koha/Auth/Hostname.t @@ -0,0 +1,139 @@ +#!/usr/bin/perl + +# Copyright Koha Community 2026 +# +# This file is part of Koha +# +# Koha 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 3 of the License, or +# (at your option) any later version. +# +# Koha 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 Koha; if not, see . + +use Modern::Perl; + +use Test::NoWarnings; +use Test::More tests => 4; + +use Koha::Auth::Hostname; +use Koha::Auth::Hostnames; + +use t::lib::TestBuilder; +use t::lib::Mocks; + +my $schema = Koha::Database->new->schema; +my $builder = t::lib::TestBuilder->new; + +subtest 'sync_from_syspref() tests' => sub { + + plan tests => 12; + + $schema->storage->txn_begin; + + Koha::Auth::Hostnames->search->delete; + + # OPACBaseURL maps to hostname_id=1 + t::lib::Mocks::mock_preference( 'OPACBaseURL', 'https://opac.example.com' ); + Koha::Auth::Hostname->sync_from_syspref( 'OPACBaseURL', 'https://opac.example.com' ); + + my $opac = Koha::Auth::Hostnames->find(1); + ok( $opac, 'OPACBaseURL creates hostname_id=1' ); + is( $opac->hostname, 'opac.example.com', 'hostname_id=1 has correct hostname' ); + + # staffClientBaseURL maps to hostname_id=2 + Koha::Auth::Hostname->sync_from_syspref( 'staffClientBaseURL', 'https://staff.example.com' ); + + my $staff = Koha::Auth::Hostnames->find(2); + ok( $staff, 'staffClientBaseURL creates hostname_id=2' ); + is( $staff->hostname, 'staff.example.com', 'hostname_id=2 has correct hostname' ); + + # Updating the URL changes the stored hostname in-place + Koha::Auth::Hostname->sync_from_syspref( 'OPACBaseURL', 'https://newopac.example.com' ); + $opac->discard_changes; + is( $opac->hostname, 'newopac.example.com', 'hostname_id=1 updated when OPACBaseURL changes' ); + is( Koha::Auth::Hostnames->search( { hostname_id => 1 } )->count, 1, 'Still only one row for hostname_id=1' ); + + # Idempotent: calling again with the same value is a no-op + Koha::Auth::Hostname->sync_from_syspref( 'OPACBaseURL', 'https://newopac.example.com' ); + is( + Koha::Auth::Hostnames->search( { hostname => 'newopac.example.com' } )->count, 1, + 'Idempotent: no duplicate created' + ); + + # http:// URLs are supported + Koha::Auth::Hostname->sync_from_syspref( 'OPACBaseURL', 'http://opac.example.org' ); + $opac->discard_changes; + is( $opac->hostname, 'opac.example.org', 'http:// URL extracted correctly' ); + + # Port numbers are stripped + Koha::Auth::Hostname->sync_from_syspref( 'OPACBaseURL', 'https://opac.example.net:8080/path' ); + $opac->discard_changes; + is( $opac->hostname, 'opac.example.net', 'Port number stripped from hostname' ); + + # Empty value is a no-op + my $hostname_before = $opac->hostname; + Koha::Auth::Hostname->sync_from_syspref( 'OPACBaseURL', '' ); + $opac->discard_changes; + is( $opac->hostname, $hostname_before, 'Empty value leaves existing hostname unchanged' ); + + # Malformed URL (no protocol) is a no-op + Koha::Auth::Hostname->sync_from_syspref( 'OPACBaseURL', 'not-a-valid-url' ); + $opac->discard_changes; + is( $opac->hostname, $hostname_before, 'Malformed URL leaves existing hostname unchanged' ); + + # Unknown preference name is ignored + Koha::Auth::Hostname->sync_from_syspref( 'SomeOtherPref', 'https://other.example.com' ); + is( + Koha::Auth::Hostnames->search( { hostname => 'other.example.com' } )->count, + 0, 'Unknown syspref name is ignored' + ); + + $schema->storage->txn_rollback; +}; + +subtest 'sync_from_sysprefs() tests' => sub { + + plan tests => 4; + + $schema->storage->txn_begin; + + Koha::Auth::Hostnames->search->delete; + + t::lib::Mocks::mock_preference( 'OPACBaseURL', 'https://opac.example.com' ); + t::lib::Mocks::mock_preference( 'staffClientBaseURL', 'https://staff.example.com' ); + + Koha::Auth::Hostname->sync_from_sysprefs; + + my $opac = Koha::Auth::Hostnames->find(1); + my $staff = Koha::Auth::Hostnames->find(2); + + ok( $opac, 'hostname_id=1 exists after sync_from_sysprefs' ); + is( $opac->hostname, 'opac.example.com', 'hostname_id=1 has OPAC hostname' ); + + ok( $staff, 'hostname_id=2 exists after sync_from_sysprefs' ); + is( $staff->hostname, 'staff.example.com', 'hostname_id=2 has staff hostname' ); + + $schema->storage->txn_rollback; +}; + +subtest 'Koha::Auth::Hostnames type tests' => sub { + + plan tests => 2; + + $schema->storage->txn_begin; + + my $hostname = $builder->build_object( { class => 'Koha::Auth::Hostnames' } ); + is( ref($hostname), 'Koha::Auth::Hostname', 'build_object returns correct object type' ); + + my $hostnames = Koha::Auth::Hostnames->search( { hostname_id => $hostname->hostname_id } ); + is( ref($hostnames), 'Koha::Auth::Hostnames', 'search returns correct collection type' ); + + $schema->storage->txn_rollback; +}; diff --git a/t/db_dependent/Koha/Auth/Identity/Provider.t b/t/db_dependent/Koha/Auth/Identity/Provider.t index 8fe84cb8982..04e1decb374 100755 --- a/t/db_dependent/Koha/Auth/Identity/Provider.t +++ b/t/db_dependent/Koha/Auth/Identity/Provider.t @@ -20,13 +20,17 @@ use Modern::Perl; use Test::NoWarnings; -use Test::More tests => 7; +use Test::More tests => 8; use Test::MockModule; use Test::Exception; use JSON qw(encode_json); +use Koha::Auth::Hostnames; +use Koha::Auth::Identity::Provider; +use Koha::Auth::Identity::Provider::Hostnames; +use Koha::Auth::Identity::Provider::Mappings; use Koha::Auth::Identity::Providers; use t::lib::TestBuilder; @@ -88,7 +92,6 @@ subtest 'set_config() tests' => sub { my $provider = $builder->build_object( { class => 'Koha::Auth::Identity::Providers', value => { protocol => 'OIDC' } } ); - $provider = $provider->upgrade_class; my $config = { key => 'key', @@ -114,7 +117,6 @@ subtest 'set_config() tests' => sub { my $provider = $builder->build_object( { class => 'Koha::Auth::Identity::Providers', value => { protocol => 'OAuth' } } ); - $provider = $provider->upgrade_class; my $config = { key => 'key', @@ -135,15 +137,15 @@ subtest 'set_config() tests' => sub { is_deeply( $provider->get_config, $config, 'Configuration stored correctly' ); }; - subtest 'Unsupported protocol tests' => sub { + subtest 'Base class (unsupported protocol) tests' => sub { plan tests => 2; - my $provider = - $builder->build_object( { class => 'Koha::Auth::Identity::Providers', value => { protocol => 'CAS' } } ); + # Cannot build in DB with invalid protocol; instantiate base class directly + my $provider = Koha::Auth::Identity::Provider->new( { protocol => 'OAuth' } ); - throws_ok { $provider->set_config() } - 'Koha::Exception', 'Exception thrown on unsupported protocol'; + throws_ok { $provider->set_config( {} ) } + 'Koha::Exception', 'Exception thrown when calling set_config on base class'; like( "$@", qr/This method needs to be subclassed/, 'Message is correct' ); }; @@ -151,48 +153,66 @@ subtest 'set_config() tests' => sub { $schema->storage->txn_rollback; }; -subtest 'get_mapping() tests' => sub { +subtest 'mappings() tests' => sub { - plan tests => 2; + plan tests => 3; $schema->storage->txn_begin; - my $provider = $builder->build_object( { class => 'Koha::Auth::Identity::Providers', value => { config => '{' } } ); - - throws_ok { $provider->get_mapping() } - 'Koha::Exceptions::Object::BadValue', 'Expected exception thrown on bad JSON'; - - my $mapping = { some => 'value', and => 'another' }; - $provider->mapping( encode_json($mapping) )->store; - - is_deeply( $provider->get_mapping, $mapping, 'Mapping correctly retrieved' ); + my $provider = $builder->build_object( { class => 'Koha::Auth::Identity::Providers' } ); + my $mappings = $provider->mappings; + + is( ref($mappings), 'Koha::Auth::Identity::Provider::Mappings', 'Type is correct' ); + is( $mappings->count, 0, 'No mappings defined' ); + + $builder->build( + { + source => 'IdentityProviderMapping', + value => { identity_provider_id => $provider->id, koha_field => 'userid', provider_field => 'uid' } + } + ); + $builder->build( + { + source => 'IdentityProviderMapping', + value => { identity_provider_id => $provider->id, koha_field => 'email', provider_field => 'mail' } + } + ); + + is( $provider->mappings->count, 2, 'The provider has 2 mappings defined' ); $schema->storage->txn_rollback; }; -subtest 'set_mapping() tests' => sub { +subtest 'hostnames() tests' => sub { - plan tests => 1; + plan tests => 3; $schema->storage->txn_begin; - my $provider = $builder->build_object( { class => 'Koha::Auth::Identity::Providers' } ); + my $provider = $builder->build_object( { class => 'Koha::Auth::Identity::Providers' } ); + my $hostnames = $provider->hostnames; - my $mapping = { some => 'value', and => 'another' }; - $provider->set_mapping($mapping)->store; + is( ref($hostnames), 'Koha::Auth::Identity::Provider::Hostnames', 'Type is correct' ); + is( $hostnames->count, 0, 'No hostnames defined' ); + + $builder->build_object( + { class => 'Koha::Auth::Identity::Provider::Hostnames', value => { identity_provider_id => $provider->id } } ); + $builder->build_object( + { class => 'Koha::Auth::Identity::Provider::Hostnames', value => { identity_provider_id => $provider->id } } ); - is_deeply( $provider->get_mapping, $mapping, 'Mapping correctly retrieved' ); + is( $provider->hostnames->count, 2, 'The provider has 2 hostnames defined' ); $schema->storage->txn_rollback; }; -subtest 'upgrade_class() tests' => sub { +subtest 'polymorphic_retrieval() tests' => sub { - plan tests => 5; + plan tests => 7; $schema->storage->txn_begin; - my $mapping = Koha::Auth::Identity::Provider::protocol_to_class_mapping; + my $providers = Koha::Auth::Identity::Providers->new; + my $mapping = $providers->_polymorphic_map; my @protocols = keys %{$mapping}; foreach my $protocol (@protocols) { @@ -204,20 +224,82 @@ subtest 'upgrade_class() tests' => sub { } ); - is( ref($provider), 'Koha::Auth::Identity::Provider', "Base class used for $protocol" ); + is( ref($provider), $mapping->{$protocol}, "build_object returns correct subclass for $protocol" ); - # upgrade - $provider = $provider->upgrade_class; - is( - ref($provider), $mapping->{$protocol}, - "Class upgraded to " . $mapping->{$protocol} . "for protocol $protocol" - ); + my $found = Koha::Auth::Identity::Providers->find( $provider->id ); + is( ref($found), $mapping->{$protocol}, "Providers->find returns correct subclass for $protocol" ); } my $provider = Koha::Auth::Identity::Provider->new( { protocol => 'Invalid' } ); - throws_ok { $provider->upgrade } - 'Koha::Exception', - 'Exception throw on invalid protocol'; + throws_ok { $provider->set_config( {} ) } 'Koha::Exception', 'Exception thrown calling set_config on base class'; + + $schema->storage->txn_rollback; +}; + +subtest 'find_forced_provider() tests' => sub { + + plan tests => 8; + + $schema->storage->txn_begin; + + # No hostname argument returns undef + is( Koha::Auth::Identity::Providers->find_forced_provider(undef), undef, 'Returns undef when hostname is undef' ); + is( + Koha::Auth::Identity::Providers->find_forced_provider(''), undef, + 'Returns undef when hostname is empty string' + ); + + my $provider = $builder->build_object( + { class => 'Koha::Auth::Identity::Providers', value => { protocol => 'OIDC', enabled => 1 } } ); + + my $hostname_record = + $builder->build_object( { class => 'Koha::Auth::Hostnames', value => { hostname => 'sso.example.com' } } ); + + # Hostname linked but force_sso=0 (default) + $builder->build_object( + { + class => 'Koha::Auth::Identity::Provider::Hostnames', + value => { + identity_provider_id => $provider->id, + hostname_id => $hostname_record->hostname_id, + is_enabled => 1, + force_sso => 0, + } + } + ); + is( + Koha::Auth::Identity::Providers->find_forced_provider('sso.example.com'), + undef, 'Returns undef when hostname is linked but force_sso is 0' + ); + + # Update to force_sso=1 but is_enabled=0 on the hostname link + Koha::Auth::Identity::Provider::Hostnames->search( + { identity_provider_id => $provider->id, hostname_id => $hostname_record->hostname_id } ) + ->update( { force_sso => 1, is_enabled => 0 } ); + is( + Koha::Auth::Identity::Providers->find_forced_provider('sso.example.com'), + undef, 'Returns undef when hostname link has is_enabled=0' + ); + + # Enable the hostname link but disable the provider itself + Koha::Auth::Identity::Provider::Hostnames->search( + { identity_provider_id => $provider->id, hostname_id => $hostname_record->hostname_id } ) + ->update( { is_enabled => 1 } ); + $provider->update( { enabled => 0 } ); + is( + Koha::Auth::Identity::Providers->find_forced_provider('sso.example.com'), + undef, 'Returns undef when provider itself is disabled' + ); + + # Re-enable the provider - now all conditions met + $provider->update( { enabled => 1 } ); + my $forced = Koha::Auth::Identity::Providers->find_forced_provider('sso.example.com'); + ok( $forced, 'Returns provider when hostname linked with force_sso=1, is_enabled=1, and provider enabled=1' ); + is( + ref($forced), 'Koha::Auth::Identity::Provider::OIDC', + 'Returned object is the correct Identity Provider subclass' + ); + is( $forced->id, $provider->id, 'Returns the correct provider' ); $schema->storage->txn_rollback; }; diff --git a/t/db_dependent/Koha/Auth/Identity/Provider/Mapping.t b/t/db_dependent/Koha/Auth/Identity/Provider/Mapping.t new file mode 100755 index 00000000000..7c3b7e6ccdc --- /dev/null +++ b/t/db_dependent/Koha/Auth/Identity/Provider/Mapping.t @@ -0,0 +1,207 @@ +#!/usr/bin/perl + +# Copyright Koha Community 2026 +# +# This file is part of Koha +# +# Koha 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 3 of the License, or +# (at your option) any later version. +# +# Koha 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 Koha; if not, see . + +use Modern::Perl; + +use Test::NoWarnings; +use Test::More tests => 4; +use Test::Exception; + +use Koha::Auth::Identity::Provider::Mapping; +use Koha::Auth::Identity::Provider::Mappings; +use Koha::Auth::Identity::Providers; + +use t::lib::TestBuilder; + +my $schema = Koha::Database->new->schema; +my $builder = t::lib::TestBuilder->new; + +subtest 'Mapping::store() tests' => sub { + + plan tests => 8; + + $schema->storage->txn_begin; + + my $provider = $builder->build_object( { class => 'Koha::Auth::Identity::Providers' } ); + + # Must throw if neither provider_field nor default_content is set + throws_ok { + Koha::Auth::Identity::Provider::Mapping->new( + { + identity_provider_id => $provider->id, + koha_field => 'userid', + } + )->store; + } + 'Koha::Exceptions::MissingParameter', + 'Exception thrown when neither provider_field nor default_content is provided'; + + like( $@->parameter, qr/provider_field or default_content/, 'Exception parameter message is correct' ); + + # Succeeds with provider_field only + my $m1 = Koha::Auth::Identity::Provider::Mapping->new( + { + identity_provider_id => $provider->id, + koha_field => 'userid', + provider_field => 'uid', + } + )->store; + ok( $m1, 'store() succeeds when only provider_field is supplied' ); + + # Succeeds with default_content only + my $m2 = Koha::Auth::Identity::Provider::Mapping->new( + { + identity_provider_id => $provider->id, + koha_field => 'categorycode', + default_content => 'PT', + } + )->store; + ok( $m2, 'store() succeeds when only default_content is supplied' ); + + # Succeeds with both provider_field and default_content + my $m3 = Koha::Auth::Identity::Provider::Mapping->new( + { + identity_provider_id => $provider->id, + koha_field => 'branchcode', + provider_field => 'branch', + default_content => 'CPL', + } + )->store; + ok( $m3, 'store() succeeds when both provider_field and default_content are supplied' ); + + # Setting is_matchpoint=1 clears any existing matchpoint for the same provider + my $first_mp = Koha::Auth::Identity::Provider::Mapping->new( + { + identity_provider_id => $provider->id, + koha_field => 'email', + provider_field => 'mail', + is_matchpoint => 1, + } + )->store; + is( $first_mp->is_matchpoint, 1, 'First matchpoint stored correctly' ); + + my $second_mp = Koha::Auth::Identity::Provider::Mapping->new( + { + identity_provider_id => $provider->id, + koha_field => 'surname', + provider_field => 'sn', + is_matchpoint => 1, + } + )->store; + + $first_mp->discard_changes; + is( $first_mp->is_matchpoint, 0, 'Previous matchpoint cleared when new matchpoint is stored' ); + is( $second_mp->is_matchpoint, 1, 'New matchpoint is active' ); + + $schema->storage->txn_rollback; +}; + +subtest 'Mappings::get_matchpoint() tests' => sub { + + plan tests => 3; + + $schema->storage->txn_begin; + + my $provider = $builder->build_object( { class => 'Koha::Auth::Identity::Providers' } ); + + is( $provider->mappings->get_matchpoint, undef, 'Returns undef when provider has no mappings' ); + + # Add a non-matchpoint mapping via builder (bypasses store() constraint) + $builder->build( + { + source => 'IdentityProviderMapping', + value => { + identity_provider_id => $provider->id, + koha_field => 'userid', + provider_field => 'uid', + is_matchpoint => 0, + } + } + ); + is( $provider->mappings->get_matchpoint, undef, 'Returns undef when no mapping is flagged as matchpoint' ); + + # Add a matchpoint mapping + $builder->build( + { + source => 'IdentityProviderMapping', + value => { + identity_provider_id => $provider->id, + koha_field => 'email', + provider_field => 'mail', + is_matchpoint => 1, + } + } + ); + my $matchpoint = $provider->mappings->get_matchpoint; + is( $matchpoint->koha_field, 'email', 'Returns the mapping flagged as matchpoint' ); + + $schema->storage->txn_rollback; +}; + +subtest 'Mappings::as_auth_mapping() tests' => sub { + + plan tests => 4; + + $schema->storage->txn_begin; + + my $provider = $builder->build_object( { class => 'Koha::Auth::Identity::Providers' } ); + + my ( $mapping, $matchpoint ) = $provider->mappings->as_auth_mapping; + is_deeply( $mapping, {}, 'Returns empty hashref when provider has no mappings' ); + is( $matchpoint, undef, 'Returns undef matchpoint when provider has no matchpoint' ); + + # Create mappings: one with provider_field (matchpoint), one with default_content only + $builder->build( + { + source => 'IdentityProviderMapping', + value => { + identity_provider_id => $provider->id, + koha_field => 'userid', + provider_field => 'uid', + default_content => undef, + is_matchpoint => 1, + } + } + ); + $builder->build( + { + source => 'IdentityProviderMapping', + value => { + identity_provider_id => $provider->id, + koha_field => 'categorycode', + provider_field => undef, + default_content => 'PT', + is_matchpoint => 0, + } + } + ); + + ( $mapping, $matchpoint ) = $provider->mappings->as_auth_mapping; + is_deeply( + $mapping, + { + userid => { is => 'uid', content => undef }, + categorycode => { is => undef, content => 'PT' }, + }, + 'Returns correct mapping hashref with is/content keys for each koha_field' + ); + is( $matchpoint, 'userid', 'Returns the koha_field name of the matchpoint mapping' ); + + $schema->storage->txn_rollback; +}; -- 2.53.0