1package OpenXPKI::Test; 2use Moose; 3use utf8; 4 5=head1 NAME 6 7OpenXPKI::Test - Set up an OpenXPKI test environment. 8 9=head1 SYNOPSIS 10 11Basic test environment: 12 13 my $oxitest = OpenXPKI::Test->new; 14 15Start an OpenXPKI test server: 16 17 my $oxitest = OpenXPKI::Test->new(with => [ qw( SampleConfig Server ) ]); 18 my $client = $oxitest->new_client_tester; 19 # $client is a "OpenXPKI::Test::QA::Role::Server::ClientHelper" 20 $client->connect; 21 $client->init_session; 22 $client->login("caop"); 23 24=head1 DESCRIPTION 25 26This class is the central new (as of 2017) test vehicle for OpenXPKI that sets 27up a separate test environment where all configuration data resides in a 28temporary directory C<$oxitest-E<gt>testenv_root."/usr/local/etc/openxpki/config.d">. 29 30Methods of this class do not execute any tests themselves, i.e. do not increment 31the test count of C<Test::More>. 32 33Tests in OpenXPKI are split into two groups: 34 35=over 36 37=item * I<unit tests> in C<core/server/t/> that test single classes and limited 38functionality and don't need a running server or a complete configuration. 39 40=item * I<QA tests> in C<qatest/> that need a running server or a more complete 41test configuration. 42 43=back 44 45This class can be used for both types of tests. 46 47To set up a basic test environment, just do 48 49 my $oxitest = OpenXPKI::Test->new; 50 51This provides the following OpenXPKI context objects: 52 53 CTX('config') 54 CTX('log') 55 CTX('dbi') 56 CTX('api2') 57 CTX('authentication') 58 CTX('session') # in-memory 59 CTX('notification') # mockup 60 61The session PKI realm is set to I<TestRealm> and the user role to I<User>. 62 63At this point, various more complex functions (e.g. crypto operations) will not 64be available, but the test environment can be extended via: 65 66=over 67 68=item * B<additional configuration entries> (constructor parameter 69C<add_config>) 70 71=item * B<additional OpenXPKI context objects> that should be initialized 72(constructor parameter C<also_init>) 73 74=item * B<Moose roles> to apply to C<OpenXPKI::Test> that provide more complex 75extensions (constructor parameter C<with>) 76 77=back 78 79For more details, see the L<constructor documentation|/new>. 80 81=head2 More complex tests via Moose roles 82 83The existing roles add more complex configuration and initialization to test 84more functions. They can easily be applied by using the L<constructor|/new> 85parameter C<with>. 86 87Available Moose roles can be found at these two locations: 88 891. C<core/server/t/lib/OpenXPKI/Test/Role> (roles for unit tests and QA 90tests): 91 92=over 93 94=item * L<CryptoLayer|OpenXPKI::Test::Role::CryptoLayer> 95 96=item * L<TestRealms|OpenXPKI::Test::Role::TestRealms> 97 98=back 99 1002. C<qatest/lib/OpenXPKI/Test/QA/Role/> (roles exclusively for QA tests): 101 102=over 103 104=item * L<SampleConfig|OpenXPKI::Test::QA::Role::SampleConfig> 105 106=item * L<Server|OpenXPKI::Test::QA::Role::Server> 107 108=item * L<WorkflowCreateCert|OpenXPKI::Test::QA::Role::WorkflowCreateCert> 109 110=item * L<Workflows|OpenXPKI::Test::QA::Role::Workflows> 111 112=back 113 114PLEASE NOTE: tests currently still use the production database but it is planned 115to use a separate SQLite DB for all tests in the future. 116 117B<Examples:> 118 119Additionally provide C<CTX('crypto_layer')>: 120 121 my $oxitest = OpenXPKI::Test->new(with => "CryptoLayer"); 122 123Use default configuration shipped with OpenXPKI and start a test server (only 124available for QA tests): 125 126 my $oxitest = OpenXPKI::Test->new(with => [ qw( SampleConfig Server ) ]); 127 128=head2 Debugging 129 130To display debug statements just use L<OpenXPKI::Debug> in your test files 131B<before> you use C<OpenXPKI::Test>: 132 133 # e.g. in t/mytest.t 134 use strict; 135 use warnings; 136 137 use Test::More; 138 139 use OpenXPKI::Debug; 140 BEGIN { $OpenXPKI::Debug::LEVEL{'OpenXPKI::Server::Database.*'} = 0b1111111 } 141 142 use OpenXPKI::Test; 143 144=cut 145 146# Core modules 147use Data::Dumper; 148use File::Path qw( remove_tree ); 149use File::Temp qw( tempdir ); 150use Module::Load qw( autoload ); 151 152# CPAN modules 153use Moose::Exporter; 154use Moose::Util; 155use Moose::Meta::Class; 156use Moose::Util::TypeConstraints; 157use Test::More; 158use Test::Deep::NoTest qw( eq_deeply bag ); # use eq_deeply() without beeing in a test 159use Digest::SHA; 160use MIME::Base64; 161use YAML::Tiny; 162 163# Project modules 164use OpenXPKI::Config; 165use OpenXPKI::Log4perl; 166use Log::Log4perl::Appender; 167use Log::Log4perl::Filter::MDC; 168use Log::Log4perl::Layout::NoopLayout; 169use OpenXPKI::MooseParams; 170use OpenXPKI::Server::Database; 171use OpenXPKI::Server::Context; 172use OpenXPKI::Server::Init; 173use OpenXPKI::Server::Log; 174use OpenXPKI::Server::Session; 175use OpenXPKI::Test::ConfigWriter; 176use OpenXPKI::Test::CertHelper::Database; 177use OpenXPKI::Test::Log4perlCallerFilter; 178 179Moose::Exporter->setup_import_methods( 180 as_is => [ \&OpenXPKI::Server::Context::CTX ], 181); 182 183subtype 'TestArrayRefOrStr', as 'ArrayRef[Any]'; 184coerce 'TestArrayRefOrStr', from 'Str', via { [ $_ ] }; 185 186=head1 DESCRIPTION 187 188=head2 Database 189 190C<OpenXPKI::Test> tries to read the following sources to determine the database 191connection parameters and stops as soon as it can find some: 192 193=over 194 195=item 1. Constructor attribute C<db_conf>. 196 197=item 2. I</usr/local/etc/openxpki/config.d/system/database.yaml>. This can be prevented 198by setting C<force_test_db =E<gt> 1>. 199 200=item 3. Environment variables C<$ENV{OXI_TEST_DB_MYSQL_XXX}>. 201 202=back 203 204If no database parameters are found anywhere it dies with an error. 205 206=head1 METHODS 207 208=head2 new 209 210Constructor. 211 212B<Parameters> (these are Moose attributes and can be accessed as such) 213 214=over 215 216=item * I<with> (optional) - Scalar or ArrayRef containing the full package or 217last part of Moose roles to apply to C<OpenXPKI::Test>. Currently the 218following names might be specified: 219 220For unit tests (I<core/server/t/>) or QA tests (I<qatest/>): 221 222=over 223 224=item * L<CryptoLayer|OpenXPKI::Test::Role::CryptoLayer> - also init 225C<CTX('crypto_layer')> 226 227=item * L<TestRealms|OpenXPKI::Test::Role::TestRealms> - add test realms 228I<alpha>, I<beta> and I<gamma> to configuration 229 230=back 231 232Only for QA tests (I<qatest/>): 233 234=over 235 236=item * L<SampleConfig|OpenXPKI::Test::QA::Role::SampleConfig> - use 237the complete default configuration shipped with OpenXPKI (slightly modified) 238 239=item * L<Server|OpenXPKI::Test::QA::Role::Server> - run OpenXPKI as a 240background server daemon and talk to it via client (socket) 241 242=item * L<Workflows|OpenXPKI::Test::QA::Role::Workflows> - also init 243C<CTX('workflow_factory')> and provide some helper methods 244 245=item * L<WorkflowCreateCert|OpenXPKI::Test::QA::Role::WorkflowCreateCert> 246- easily create test certificates 247 248=back 249 250For each given string C<NAME> the following packages are tried for Moose role 251application: C<NAME> (unmodified string), C<OpenXPKI::Test::Role::NAME>, 252C<OpenXPKI::Test::QA::Role::NAME> 253 254=item * I<add_config> (optional) - I<HashRef> with additional configuration 255entries that complement or replace the default config. 256 257Keys are the dot separated configuration paths, values are HashRefs or YAML 258strings with the actual configuration data that will be merged (and finally 259converted into YAML and stored on disk in a temporary directory). 260 261Example: 262 263 OpenXPKI::Test->new( 264 add_config => { 265 "realm.alpha.auth.handler.Signature1" => { 266 realm => [ "alpha" ], 267 cacert => [ "MyCertId" ], 268 }, 269 # or: 270 "realm.alpha.auth.handler.Signature2" => " 271 realm: 272 - alpha 273 cacert: 274 - MyCertId 275 ", 276 } 277 ); 278 279This would write the following content into I<etc/openxpki/config.d/realm/alpha.yaml> 280(below C<$oxitest-E<gt>testenv_root>): 281 282 ... 283 Signature 284 realm: 285 - alpha 286 cacert: 287 - MyCertId 288 ... 289 290=cut 291has user_config => ( 292 is => 'rw', 293 isa => 'HashRef', 294 init_arg => 'add_config', 295 lazy => 1, 296 default => sub { 297 my $self = shift; 298 $self->has_user_config_sub ? $self->user_config_sub->($self) : {}; 299 }, 300); 301 302=item * I<add_config_sub> (optional) - intead of C<add_config> specifies a 303I<CodeRef> which must return a I<HashRef> with additional configuration 304entries. 305 306The specified sub receives the C<OpenXPKI::Test> object as first parameter, so 307it is able to access other configuration entries. 308 309Please note that you CANNOT specify both C<add_config> and C<add_config_sub>. 310 311Example: 312 313 OpenXPKI::Test->new( 314 add_config_sub => sub { 315 my $test = shift; 316 return { 317 "some.special.entry" => $test->testenv_root . "/mydir"; 318 }; 319 } 320 ); 321 322=cut 323has user_config_sub => ( 324 is => 'rw', 325 isa => 'CodeRef', 326 init_arg => 'add_config_sub', 327 predicate => 'has_user_config_sub', 328); 329 330=item * I<also_init> (optional) - ArrayRef (or Str) of additional init tasks 331that the OpenXPKI server shall perform. 332 333You have to make sure (e.g. by adding additional config entries) that the 334prerequisites for each task are met. 335 336=cut 337has also_init => ( 338 is => 'rw', 339 isa => 'TestArrayRefOrStr', 340 lazy => 1, 341 coerce => 1, 342 default => sub { [] }, 343); 344 345=item * I<db_conf> (optional) - Database configuration (I<HashRef>). 346 347Per default the configuration is read from an existing configuration file 348(below I</usr/local/etc/openxpki>) or environment variables. 349 350=cut 351has db_conf => ( 352 is => 'rw', 353 isa => 'HashRef', 354 lazy => 1, 355 builder => '_build_db_conf', 356 predicate => 'has_db_conf', 357 trigger => sub { 358 my ($self, $new, $old) = @_; 359 my @keys = qw( type name user passwd ); 360 die "Required keys missing for 'db_conf': ".join(", ", grep { not defined $new->{$_} } @keys) 361 unless eq_deeply([keys %$new], bag(@keys)); 362 }, 363); 364 365=item * I<dbi> (optional) - instance of L<OpenXPKI::Server::Database>. 366 367Per default it is initialized with a new instance using C<$self-E<gt>db_conf>. 368 369=cut 370has dbi => ( 371 is => 'rw', 372 isa => 'OpenXPKI::Server::Database', 373 lazy => 1, 374 builder => '_build_dbi', 375); 376 377=item * I<force_test_db> - Set to 1 to prevent the try to read database config 378from existing configuration file and only read it from environment variables 379C<$ENV{OXI_TEST_DB_MYSQL_XXX}>. 380 381=cut 382has force_test_db => ( 383 is => 'rw', 384 isa => 'Bool', 385 lazy => 1, 386 default => 0, 387); 388 389=item * I<testenv_root> (optional) - Temporary directory that serves as root 390path for the test environment (configuration files etc.). Default: newly created 391directory that will be deleted on object destruction 392 393=cut 394has testenv_root => ( 395 is => 'rw', 396 isa => 'Str', 397 lazy => 1, 398 default => sub { scalar(tempdir( CLEANUP => 0 )) }, # "CLEANUP => 1" would interfere with forked processes (Watchdog) 399 # set flag if attribute was set via constructor 400 initializer => sub { 401 my ($self, $value, $callback, $attr) = @_; 402 $self->_custom_testenv_root(1); 403 $callback->($value); 404 }, 405); 406has _custom_testenv_root => ( is => 'rw', isa => 'Bool', lazy => 1, default => 0 ); 407 408=item * I<log_level> (optional) - L<Log::Log4Perl> log level for screen output. 409This is only relevant if C<$ENV{TEST_VERBOSE}> is set, i.e. user calls C<prove -v ...>. 410Otherwise logging will be disabled anyway. Default: WARN 411 412=cut 413has log_level => ( 414 is => 'rw', 415 isa => 'Str', 416 default => "WARN", 417); 418 419=item * I<log_class> (optional) - A regex: only show log messages originating 420from Perl packages matching this value. 421 422E.g. C<log_class =E<gt> qr/^OpenXPKI::Client::UI/> to show messages from all 423packages below this namespace. Default: qr/^/ (= all packages) 424 425=cut 426has log_class => ( 427 is => 'rw', 428 isa => 'Regexp', 429 default => sub { qr/^/ }, 430); 431 432=item * I<enable_workflow_log> (optional) - if set to 1 workflow related log 433entries will be written into the database. This allows e.g. for querying the 434workflow log / history. 435 436Per default when using this test class there is only screen logging. 437 438=cut 439has enable_workflow_log => ( 440 is => 'rw', 441 isa => 'Bool', 442 default => 0, 443); 444 445=item * I<enable_file_log> - if set to 1 all log messages above log level DEBUG 446are written to a temporary file for manual inspection. 447 448Also see L</log_path> and L</diag_log>. 449 450=cut 451has enable_file_log => ( 452 is => 'rw', 453 isa => 'Bool', 454 default => 0, 455); 456 457 458=back 459 460=head2 certhelper_database 461 462Returns an instance of L<OpenXPKI::Test::CertHelper::Database> with the database 463configuration set to C<$self-E<gt>db_conf>. 464 465=cut 466has certhelper_database => ( 467 is => 'rw', 468 isa => 'OpenXPKI::Test::CertHelper::Database', 469 lazy => 1, 470 default => sub { OpenXPKI::Test::CertHelper::Database->new }, 471); 472 473=head2 config_writer 474 475Returns an instance of L<OpenXPKI::Test::ConfigWriter>. 476 477=cut 478has config_writer => ( 479 is => 'rw', 480 isa => 'OpenXPKI::Test::ConfigWriter', 481 lazy => 1, 482 default => sub { 483 my $self = shift; 484 OpenXPKI::Test::ConfigWriter->new( 485 basedir => $self->testenv_root, 486 ) 487 }, 488 handles => { 489 add_conf => "add_config", 490 get_conf => "get_config_node", 491 }, 492); 493=head2 add_conf 494 495Just a shortcut to L<OpenXPKI::Test::ConfigWriter/add_config>. 496 497=head2 get_conf 498 499Just a shortcut to L<OpenXPKI::Test::ConfigWriter/get_config_node>. 500 501=cut 502 503 504=head2 default_realm 505 506Returns the configured default realm. 507 508=cut 509has default_realm => ( 510 is => 'rw', 511 isa => 'Str', 512 init_arg => undef, 513 predicate => 'has_default_realm', 514); 515 516=head2 session 517 518Returns the session context object C<CTX('session')> once L</init_server> was 519called. 520 521=cut 522has session => ( 523 is => 'rw', 524 isa => 'Object', 525 init_arg => undef, 526 predicate => 'has_session', 527); 528 529=head2 log_path 530 531Returns the path to the log file if the constructor has been called with 532C<enable_file_log =E<gt> 1>. 533 534=cut 535has log_path => ( 536 is => 'rw', 537 isa => 'Str', 538 lazy => 1, 539 default => sub { my $self = shift; $self->testenv_root."/openxpki.log" }, 540 init_arg => undef, 541); 542 543has path_log4perl_conf => ( is => 'rw', isa => 'Str', lazy => 1, default => sub { shift->testenv_root."/usr/local/etc/openxpki/log.conf" } ); 544has conf_log4perl => ( is => 'rw', isa => 'Str', lazy => 1, builder => "_build_log4perl" ); 545has conf_session => ( is => 'rw', isa => 'HashRef', lazy => 1, builder => "_build_conf_session" ); 546has conf_database => ( is => 'rw', isa => 'HashRef', lazy => 1, builder => "_build_conf_database" ); 547# password for all openxpki users 548has password => ( is => 'rw', isa => 'Str', lazy => 1, default => "openxpki" ); 549has password_hash => ( is => 'rw', isa => 'Str', lazy => 1, default => sub { my $self = shift; $self->_get_password_hash($self->password) } ); 550has testenv_root_pid => ( is => 'rw', isa => 'Str', init_arg => undef); 551 552around BUILDARGS => sub { 553 my $orig = shift; 554 my $class = shift; 555 my @args = @_; 556 557 if (@args % 2 == 0) { 558 my %arg_hash = @args; 559 560 if (my $roles = delete $arg_hash{with}) { 561 die "Parameter 'with' must be a Scalar or an ArrayRef of role names" if (ref $roles and ref $roles ne 'ARRAY'); 562 $roles = [ $roles ] if not ref $roles; 563 for my $shortname (@$roles) { 564 my $role; 565 # Try loading the role with given name and below both test namespaces 566 for my $namespace ("", "OpenXPKI::Test::Role::", "OpenXPKI::Test::QA::Role::") { 567 my $p = "${namespace}${shortname}"; 568 # if package is not found, autoload() dies and eval() returns 569 eval { autoload $p }; 570 if (not $@) { $role = $p; last } 571 } 572 die "Could not find test class role '$shortname'" unless $role; 573 Moose::Util::ensure_all_roles($class, $role); 574 } 575 } 576 @args = %arg_hash; 577 } 578 return $class->$orig(@args); 579}; 580 581sub BUILD { 582 my $self = shift; 583 584 $ENV{OXI_TESTENV_ROOT} = $self->testenv_root; 585 $self->testenv_root_pid($$); 586 587 $self->init_logging; 588 $self->init_base_config; 589 $self->init_user_config; 590 $self->write_config; 591 $self->init_server; 592 # 593 # Please note: if you change the following lines, add every call 594 # after $self->init_server also to 595 # OpenXPKI::Test::QA::Role::Server, modifier "around 'init_server'", the 596 # child code after $self->$orig() 597 # 598 $self->init_session_and_context; 599} 600 601sub DEMOLISH { 602 my $self = shift; 603 return unless $self->testenv_root_pid == $$; 604 if ($self->enable_file_log) { 605 diag "=========="; 606 diag "Log file was enabled: " . $self->log_path; 607 diag "Temporary directory will NOT be removed!"; 608 diag "=========="; 609 } 610 else { 611 # using "tempdir(CLEANUP => 1)" in the attribute builder would 612 # interfere with forked processes (Watchdog) 613 remove_tree($self->testenv_root) unless $self->_custom_testenv_root; 614 } 615} 616 617sub _build_log4perl { 618 my ($self, $is_early_init) = @_; 619 620 # special behaviour in CI environments: log to file 621 # (detects Travis CI/CircleCI/Gitlab CI/Appveyor/CodeShip + Jenkins/TeamCity) 622 if ($ENV{CI} or $ENV{BUILD_NUMBER}) { 623 my $logfile = $self->config_writer->path_log_file; 624 return qq( 625 log4perl.rootLogger = INFO, CatchAll 626 log4perl.category.Workflow = OFF 627 log4perl.appender.CatchAll = Log::Log4perl::Appender::File 628 log4perl.appender.CatchAll.filename = $logfile 629 log4perl.appender.CatchAll.layout = Log::Log4perl::Layout::PatternLayout 630 log4perl.appender.CatchAll.layout.ConversionPattern = %d %m [pid=%P|%i]%n 631 log4perl.appender.CatchAll.syswrite = 1 632 log4perl.appender.CatchAll.utf8 = 1 633 ); 634 } 635 # default: only log to screen 636 return $self->_log4perl_screen; 637} 638 639sub _log4perl_screen { 640 my ($self) = @_; 641 642 my $threshold_screen = $ENV{TEST_VERBOSE} ? uc($self->log_level) : 'OFF'; 643 my $log_path = $self->log_path; 644 my $log_class_re = $self->log_class; # will get stringified below 645 646 return qq( 647 log4perl.rootLogger = INFO, Screen, File 648 log4perl.category.openxpki.auth = TRACE 649 log4perl.category.openxpki.audit = TRACE 650 log4perl.category.openxpki.system = TRACE 651 log4perl.category.openxpki.workflow = TRACE 652 log4perl.category.openxpki.application = TRACE 653 log4perl.category.openxpki.deprecated = WARN 654 log4perl.category.connector = WARN 655 log4perl.category.Workflow = OFF 656 657 log4perl.filter.OxiTestFilter = OpenXPKI::Test::Log4perlCallerFilter 658 log4perl.filter.OxiTestFilter.class_re = $log_class_re 659 660 log4perl.appender.Screen = Log::Log4perl::Appender::Screen 661 log4perl.appender.Screen.layout = Log::Log4perl::Layout::PatternLayout 662 log4perl.appender.Screen.layout.ConversionPattern = # >> %m [%C]%n 663 log4perl.appender.Screen.Filter = OxiTestFilter 664 log4perl.appender.Screen.Threshold = $threshold_screen 665 666 # "File" is disabled by default 667 log4perl.appender.File = Log::Log4perl::Appender::File 668 log4perl.appender.File.filename = $log_path 669 log4perl.appender.File.syswrite = 1 670 log4perl.appender.File.utf8 = 1 671 log4perl.appender.File.layout = Log::Log4perl::Layout::PatternLayout 672 log4perl.appender.File.layout.ConversionPattern = # %d %m [pid=%P|%i]%n 673 log4perl.appender.File.Threshold = OFF 674 ); 675} 676 677sub _build_conf_session { 678 my ($self) = @_; 679 return { 680 type => "Database", 681 lifetime => "1200", 682 table => "backend_session", 683 }; 684} 685 686 687sub _build_conf_database { 688 my ($self) = @_; 689 return { 690 main => { 691 debug => 0, 692 type => $self->db_conf->{type}, 693 $self->db_conf->{host} ? ( host => $self->db_conf->{host} ) : (), 694 $self->db_conf->{port} ? ( port => $self->db_conf->{port} ) : (), 695 name => $self->db_conf->{name}, 696 user => $self->db_conf->{user}, 697 passwd => $self->db_conf->{passwd}, 698 }, 699 }; 700} 701 702=head2 init_logging 703 704B<Only called internally:> initialize logging. 705 706=cut 707sub init_logging { 708 my ($self) = @_; 709 710 note "[OpenXPKI::Test->init_logging]"; 711 712 OpenXPKI::Log4perl->init_or_fallback( \($self->_log4perl_screen) ); 713 714 # additional workflow log (database) 715 if ($self->enable_workflow_log) { 716 my $appender = Log::Log4perl::Appender->new( 717 "OpenXPKI::Server::Log::Appender::Database", 718 table => "application_log", 719 microseconds => 1, 720 ); 721 $appender->layout(Log::Log4perl::Layout::NoopLayout->new()), 722 $appender->filter(Log::Log4perl::Filter::MDC->new( 723 KeyToMatch => "wfid", 724 RegexToMatch => '\d+', 725 )); 726 Log::Log4perl->get_logger("openxpki.application")->add_appender($appender); 727 } 728 729 # additional file log 730 if ($self->enable_file_log) { 731 # We cannot use Log::Log4perl->appender_by_name("File")->threshold(uc($self->log_level)); 732 # as this accesses the actual appender class, but we need the wrapper class Log::Log4perl::Appender 733 # (https://www.perlmonks.org/?node_id=1199218) 734 $Log::Log4perl::Logger::APPENDER_BY_NAME{'File'}->threshold('DEBUG'); 735 note " >"; 736 note " > All log messages (log level DEBUG) will be written to:"; 737 note " > ".$self->log_path; 738 note " >"; 739 } 740} 741 742=head2 diag_log 743 744Outputs all log entries in the log file (only if enabled via constructor 745parameter C<enable_file_log>). 746 747Output is done via C<diag()>. 748 749=cut 750sub diag_log { 751 my ($self) = @_; 752 return unless $self->enable_file_log; 753 open my $fh, '<', $self->log_path or return; 754 755 local $/; # slurp mode 756 my $logs = <$fh>; 757 close $fh; 758 diag $logs; 759} 760 761=head2 init_base_config 762 763B<Only called internally:> pass base config entries to L<OpenXPKI::Test::ConfigWriter>. 764 765This is the standard hook for test class roles to add configuration entries. 766So in a role you can e.g. inject configuration entries as follows: 767 768 after 'init_base_config' => sub { 769 my $self = shift; 770 771 # do not overwrite existing node (e.g. inserted by other roles) 772 if (not $self->get_conf("a.b.c", 1)) { 773 $self->add_conf( 774 "a.b.c" => { 775 key => "value", 776 }, 777 ); 778 } 779 }; 780 781=cut 782sub init_base_config { 783 my ($self) = @_; 784 785 note "[OpenXPKI::Test->init_base_config]"; 786 787 $self->add_conf( 788 "system.database" => $self->conf_database, 789 "system.server.session" => $self->conf_session, 790 "system.server.log4perl" => $self->path_log4perl_conf, 791 792 # Add basic test realm. 793 # Without any realm we cannot set a user via CTX('authentication') 794 "system.realms.test" => { 795 label => "TestRealm", 796 baseurl => "http://127.0.0.1/test/", 797 }, 798 "realm.test.auth" => $self->auth_config, 799 "realm.test.workflow" => { 800 def => {}, # node is required by OpenXPKI 801 persister => { 802 # fallback default persister 803 OpenXPKI => { 804 class => "OpenXPKI::Server::Workflow::Persister::DBI", 805 }, 806 }, 807 }, 808 ); 809} 810 811=head2 init_user_config 812 813B<Only called internally:> pass additional config entries that were supplied via 814constructor parameter C<add_config> to L<OpenXPKI::Test::ConfigWriter>. 815 816=cut 817sub init_user_config { 818 my ($self) = @_; 819 820 note "[OpenXPKI::Test->init_user_config]"; 821 822 # Add user supplied config (via constructor argument "add_config") 823 for (sort keys %{ $self->user_config }) { # sorting should help adding config items deeper in the tree after those at the top 824 my $val = $self->user_config->{$_}; 825 # support config given as YAML string 826 if (ref $val eq '') { 827 $val = YAML::Tiny->read_string($val)->[0]; 828 } 829 $self->add_conf($_ => $val); 830 } 831 832 if (not $self->has_default_realm) { 833 note " Setting default realm to 'test' as no other realm was set"; 834 $self->default_realm('test'); 835 } 836 else { 837 note " Default realm: ".$self->default_realm; 838 } 839} 840 841=head2 write_config 842 843B<Only called internally:> write test configuration to disk (temporary directory). 844 845=cut 846sub write_config { 847 my ($self) = @_; 848 849 note "[OpenXPKI::Test->write_config]"; 850 851 # write configuration YAML files 852 $self->config_writer->create; 853 854 # write Log4perl config: it's OK to do this late because we already initialize Log4perl in init_logging() 855 $self->config_writer->write_str($self->path_log4perl_conf, $self->conf_log4perl); 856 857 # store private key files in temp env/dir 858 for my $cert ($self->certhelper_database->all_certs) { 859 $self->config_writer->write_private_key($cert->db->{pki_realm}, $cert->name, $cert->private_key); 860 } 861 862 # point server to the test config dir (evaluated by OpenXPKI::Config) 863 $ENV{OPENXPKI_CONF_PATH} = $self->testenv_root."/usr/local/etc/openxpki/config.d"; 864 865 # point clients to the test config dir (evaluated by OpenXPKI::Client::Config) 866 # -> CURRENTLY UNUSED as this affects only (est|rpc|scep|soap).fcgi which are not tested 867 $ENV{OPENXPKI_CLIENT_CONF_DIR} = $self->testenv_root."/usr/local/etc/openxpki"; 868} 869 870=head2 init_server 871 872B<Only called internally:> initializes the basic server context objects: 873 874 C<CTX('config')> 875 C<CTX('log')> 876 C<CTX('dbi')> 877 C<CTX('api2')> 878 C<CTX('authentication')> 879 880=cut 881sub init_server { 882 my ($self) = @_; 883 884 note "[OpenXPKI::Test->init_server]"; 885 886 OpenXPKI::Server::Context::reset(); 887 OpenXPKI::Server::Init::reset(); 888 889 # init log object (and force it to NOT reinitialize Log4perl) 890 OpenXPKI::Server::Context::setcontext({ log => OpenXPKI::Server::Log->new(CONFIG => undef) }) 891 unless OpenXPKI::Server::Context::hascontext("log"); # may already be set if multiple instances of OpenXPKI::Test are created 892 893 # init basic CTX objects 894 my @tasks = qw( config_versioned dbi_log api2 authentication ); 895 896 # init notification object if needed 897 my $cfg_notification = "realm.".$self->default_realm.".notification"; 898 if ($self->get_conf($cfg_notification, 1)) { 899 note " Config node $cfg_notification found, initializing real CTX('notification') object"; 900 push @tasks, "notification"; 901 } 902 903 # add tasks requested via constructor parameter "also_init" (or injected by roles) 904 my %task_hash = map { $_ => 1 } @tasks; 905 for (grep { not $task_hash{$_} } @{ $self->also_init }) { 906 push @tasks, $_; 907 $task_hash{$_} = 1; # prevent duplicate tasks in "also_init" 908 } 909 910 OpenXPKI::Server::Init::init({ TASKS => \@tasks, SILENT => 1, CLI => 0 }); 911 912 # use the same DB connection as the test object to be able to do COMMITS 913 # etc. in tests 914 OpenXPKI::Server::Context::setcontext({ dbi => $self->dbi }) 915 unless OpenXPKI::Server::Context::hascontext("dbi"); # may already be set if multiple instances of OpenXPKI::Test are created 916 917 # Set fake notification object if there is no real one already 918 # (either via setup above or requested by user) 919 if (not OpenXPKI::Server::Context::hascontext("notification")) { 920 note " Initializing mockup CTX('notification') object"; 921 OpenXPKI::Server::Context::setcontext({ 922 notification => 923 Moose::Meta::Class->create('OpenXPKI::Test::AnonymousClass::Notification::Mockup' => ( 924 methods => { 925 notify => sub { }, 926 }, 927 ))->new_object 928 }); 929 } 930 931} 932 933=head2 init_session_and_context 934 935B<Only called internally:> create in-memory session C<CTX('session')> and (if there 936is no other object already) a mock notification objection C<CTX('notification')>. 937 938This is the standard hook for roles to modify session data, e.g.: 939 940 after 'init_session_and_context' => sub { 941 my $self = shift; 942 $self->session->data->pki_realm("democa") if $self->has_session; 943 }; 944 945=cut 946sub init_session_and_context { 947 my ($self) = @_; 948 949 note "[OpenXPKI::Test->init_session_and_context]"; 950 951 $self->session(OpenXPKI::Server::Session->new(load_config => 1)->create); 952 953 # Set session separately (OpenXPKI::Server::Init::init "killed" any old one) 954 OpenXPKI::Server::Context::setcontext({ 955 session => $self->session, 956 force => 1, 957 }); 958 959 # set default user (after session init as CTX('session') is needed by auth handler 960 $self->set_user($self->default_realm, "user"); 961} 962 963=head2 set_user 964 965Directly sets the current PKI realm and user in the session without any login 966process. 967 968The user must exist within the authentication config path, i.e. as 969I<realm.RRR.auth.handler.HHH.user.USER>. 970 971B<Positional Parameters> 972 973=over 974 975=item * C<$realm> I<Str> - PKI realm 976 977=item * C<$user> I<Str> - username 978 979=back 980 981=cut 982sub set_user { 983 my ($self, $realm, $user) = @_; 984 985 $self->session->data->pki_realm($realm); 986 987 my $reply = OpenXPKI::Server::Context::CTX('authentication')->login_step({ 988 STACK => 'OxiTestAuthStack', 989 MESSAGE => { 990 PARAMS => { LOGIN => $user, PASSWD => $self->password }, 991 }, 992 }); 993; 994 die "Could not set user to '$user' " unless(ref $reply eq 'OpenXPKI::Server::Authentication::Handle'); 995 996 my $userid = $reply->userid; 997 my $role = $reply->role; 998 $self->session->data->user($userid); 999 $self->session->data->role($role); 1000 $self->session->is_valid(1); 1001 1002 Log::Log4perl::MDC->put('user', $userid); 1003 Log::Log4perl::MDC->put('role', $role); 1004 1005 note " session set to realm '$realm', user '$userid', role '$role' "; 1006} 1007 1008=head2 api2_command 1009 1010Executes the given API2 command and returns the result. 1011 1012Convenience method to prevent usage of C<CTX('api2')> in test files. 1013 1014B<Positional Parameters> 1015 1016=over 1017 1018=item * C<$command> I<Str> - command name 1019 1020=item * C<$params> I<HashRef> - parameters 1021 1022=back 1023 1024=cut 1025sub api2_command { 1026 my ($self, $command, $params) = @_; 1027 return OpenXPKI::Server::Context::CTX('api2')->$command($params ? (%$params) : ()); 1028} 1029 1030=head2 insert_testcerts 1031 1032Inserts all or the specified list of test certificates from 1033L<OpenXPKI::Test::CertHelper::Database> into the database. 1034 1035B<Parameters> 1036 1037=over 1038 1039=item * C<only> I<ArrayRef> - only add the given certificates (expects names like I<alpha-root-1>) 1040 1041=item * C<exclude> I<ArrayRef> - exclude the given certificates 1042 1043=back 1044 1045=cut 1046sub insert_testcerts { 1047 my ($self, %args) = named_args(\@_, 1048 exclude => { isa => 'ArrayRef', optional => 1 }, 1049 only => { isa => 'ArrayRef', optional => 1 }, 1050 ); 1051 1052 die "Either specify 'only' or 'exclude', not both." if $args{only} && $args{exclude}; 1053 1054 my $certhelper = $self->certhelper_database; 1055 my $certnames; 1056 if ($args{only}) { 1057 $certnames = $args{only}; 1058 } 1059 elsif ($args{exclude}) { 1060 my $exclude = { map { $_ => 1 } @{ $args{exclude} } }; 1061 $certnames = [ grep { not $exclude->{$_} } $certhelper->all_cert_names ]; 1062 } 1063 else { 1064 $certnames = [ $certhelper->all_cert_names ]; 1065 } 1066 1067 $self->dbi->start_txn; 1068 1069 $self->dbi->merge( 1070 into => "certificate", 1071 set => $certhelper->cert($_)->db, 1072 where => { subject_key_identifier => $certhelper->cert($_)->subject_key_id }, 1073 ) for @{ $certnames }; 1074 1075 for (@{ $certnames }) { 1076 next unless $certhelper->cert($_)->db_alias->{alias}; 1077 $self->dbi->merge( 1078 into => "aliases", 1079 set => { 1080 %{ $certhelper->cert($_)->db_alias }, 1081 identifier => $certhelper->cert($_)->db->{identifier}, 1082 notbefore => $certhelper->cert($_)->db->{notbefore}, 1083 notafter => $certhelper->cert($_)->db->{notafter}, 1084 }, 1085 where => { 1086 pki_realm => $certhelper->cert($_)->db->{pki_realm}, 1087 alias => $certhelper->cert($_)->db_alias->{alias}, 1088 }, 1089 ); 1090 } 1091 $self->dbi->commit; 1092} 1093 1094=head2 delete_all 1095 1096Deletes all test certificates from the database. 1097 1098=cut 1099sub delete_testcerts { 1100 my ($self) = @_; 1101 my $certhelper = $self->certhelper_database; 1102 1103 $self->dbi->start_txn; 1104 $self->dbi->delete(from => 'certificate', where => { identifier => $certhelper->all_cert_ids } ); 1105 $self->dbi->delete(from => 'aliases', where => { identifier => [ map { $_->db->{identifier} } $certhelper->all_certs ] } ); 1106 $self->dbi->delete(from => 'crl', where => { issuer_identifier => [ map { $_->id } $certhelper->all_certs ] } ); 1107 $self->dbi->commit; 1108} 1109 1110sub _build_dbi { 1111 my ($self) = @_; 1112 1113 #Log::Log4perl->easy_init($OFF); 1114 return OpenXPKI::Server::Database->new( 1115 # "CONFIG => undef" prevents OpenXPKI::Server::Log from re-initializing Log4perl 1116 log => OpenXPKI::Server::Log->new(CONFIG => undef)->system, 1117 db_params => $self->db_conf, 1118 ); 1119} 1120 1121# TODO Remove "force_test_db", add "sqlite", under qatest/ the default should be _db_config_from_env() and under core/server/t it should be an SQLite DB 1122sub _build_db_conf { 1123 my ($self) = @_; 1124 1125 my $conf; 1126 $conf = $self->_db_config_from_production unless $self->force_test_db; 1127 $conf ||= $self->_db_config_from_env; 1128 die "Could not read database config from /usr/local/etc/openxpki or env variables" unless $conf; 1129 return $conf; 1130} 1131 1132sub _db_config_from_production { 1133 my ($self) = @_; 1134 1135 return unless (-d "/usr/local/etc/openxpki/config.d" and -r "/usr/local/etc/openxpki/config.d"); 1136 1137 # make sure OpenXPKI::Config::Backend reads from the given LOCATION 1138 my $old_env = $ENV{OPENXPKI_CONF_PATH}; delete $ENV{OPENXPKI_CONF_PATH}; 1139 my $config = OpenXPKI::Config::Backend->new(LOCATION => "/usr/local/etc/openxpki/config.d"); 1140 $ENV{OPENXPKI_CONF_PATH} = $old_env if $old_env; 1141 1142 my $db_conf = $config->get_hash('system.database.main'); 1143 my $conf = { 1144 type => $db_conf->{type}, 1145 name => $db_conf->{name}, 1146 host => $db_conf->{host}, 1147 port => $db_conf->{port}, 1148 user => $db_conf->{user}, 1149 passwd => $db_conf->{passwd}, 1150 }; 1151 # Set environment variables 1152 my $db_env = $config->get_hash("system.database.main.environment"); 1153 $ENV{$_} = $db_env->{$_} for (keys %{$db_env}); 1154 1155 return $conf; 1156} 1157 1158sub _db_config_from_env { 1159 my ($self) = @_; 1160 1161 return unless $ENV{OXI_TEST_DB_MYSQL_NAME}; 1162 1163 return { 1164 type => "MariaDB", 1165 $ENV{OXI_TEST_DB_MYSQL_DBHOST} ? ( host => $ENV{OXI_TEST_DB_MYSQL_DBHOST} ) : (), 1166 $ENV{OXI_TEST_DB_MYSQL_DBPORT} ? ( port => $ENV{OXI_TEST_DB_MYSQL_DBPORT} ) : (), 1167 name => $ENV{OXI_TEST_DB_MYSQL_NAME}, 1168 user => $ENV{OXI_TEST_DB_MYSQL_USER}, 1169 passwd => $ENV{OXI_TEST_DB_MYSQL_PASSWORD}, 1170 1171 }; 1172} 1173 1174 1175sub _get_password_hash { 1176 my ($self, $password) = @_; 1177 my $salt = ""; 1178 $salt .= chr(int(rand(256))) for (1..3); 1179 $salt = encode_base64($salt); 1180 1181 my $ctx = Digest::SHA->new; 1182 $ctx->add($password); 1183 $ctx->add($salt); 1184 return "{ssha}".encode_base64($ctx->digest . $salt, ''); 1185} 1186 1187sub auth_config { 1188 my ($self) = @_; 1189 return { 1190 stack => { 1191 "OxiTestAuthStack" => { 1192 description => "OpenXPKI test authentication stack", 1193 handler => "OxiTestAuthHandler", 1194 type => "passwd", 1195 }, 1196 }, 1197 handler => { 1198 "OxiTestAuthHandler" => { 1199 label => "OpenXPKI test authentication handler", 1200 type => "Password", 1201 user => { 1202 # password is always "openxpki" 1203 caop => { 1204 digest => $self->password_hash, # "{ssha}JQ2BAoHQZQgecmNjGF143k4U2st6bE5B", 1205 role => "CA Operator", 1206 }, 1207 raop => { 1208 digest => $self->password_hash, 1209 role => "RA Operator", 1210 }, 1211 raop2 => { 1212 digest => $self->password_hash, 1213 role => "RA Operator", 1214 tenant => 'Tenant B', 1215 }, 1216 user => { 1217 digest => $self->password_hash, 1218 role => "User" 1219 }, 1220 user2 => { 1221 digest => $self->password_hash, 1222 role => "User", 1223 tenant => 'Tenant B', 1224 }, 1225 }, 1226 }, 1227 }, 1228 roles => { 1229 "Anonymous" => { label => "Anonymous" }, 1230 "CA Operator" => { label => "CA Operator" }, 1231 "RA Operator" => { label => "RA Operator" }, 1232 "System" => { label => "System" }, 1233 "User" => { label => "User" }, 1234 }, 1235 }; 1236} 1237 12381; 1239