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