1#!/usr/local/bin/perl
2use strict;
3use warnings;
4
5# Core modules
6use FindBin qw( $Bin );
7
8# CPAN modules
9use Test::More;
10use Test::Deep;
11use Test::Exception;
12use Data::UUID;
13use Try::Tiny;
14
15#use OpenXPKI::Debug; $OpenXPKI::Debug::LEVEL{'OpenXPKI::Server::Workflow::Persister.*'} = 32;
16
17# Project modules
18use lib "$Bin/../lib";
19use lib "$Bin";
20use OpenXPKI::Server::Context qw( CTX );
21use OpenXPKI::Test;
22
23try {
24    require OpenXPKI::Server::Workflow::Persister::Archiver;
25    plan tests => 5;
26}
27catch {
28    plan skip_all => "persister 'Archiver' no available";
29};
30
31my $wf_def = "
32head:
33    prefix: testwf
34    persister: Archiver
35
36state:
37    INITIAL:
38        action: noop > AUTO
39
40    AUTO:
41        action: set_context set_attr > LOITER
42        autorun: 1
43
44    LOITER:
45        action: noop > SUCCESS
46
47action:
48    noop:
49        class: OpenXPKI::Server::Workflow::Activity::Noop
50
51    set_context:
52        class: OpenXPKI::Server::Workflow::Activity::Tools::SetContext
53        param:
54            env: sky
55            temp: -10
56            vehicle: plane
57
58    set_attr:
59        class: OpenXPKI::Server::Workflow::Activity::Tools::SetAttribute
60        param:
61            creator: dummy
62            color: blue
63            hairstyle: bald
64            shoesize: 10
65
66acl:
67    MyFairyKing:
68        creator: any
69";
70
71#
72# Fail on wrong persister arguments
73#
74throws_ok {
75    my $oxitest = OpenXPKI::Test->new(
76        with => [ qw( TestRealms ) ],
77        also_init => "workflow_factory",
78        add_config => {
79            "realm.alpha.workflow.persister.Archiver" => "
80                class: OpenXPKI::Server::Workflow::Persister::Archiver
81                cleanup_defaults:
82                    crash: test dummies
83            ",
84        },
85    );
86} qr/defaults must contain/, "fail on wrong arguments for persister defaults";
87
88sub items_ok($@) {
89    my $testname = shift;
90    my %args = @_;
91    my $config = $args{config};
92    my $items = $args{expected};
93    my $force_failure = $args{force_failure};
94    my $fields = $config->{workflow}->{field} // {};
95    my $attributes = $config->{workflow}->{attribute} // {};
96
97    my $persister = {
98        class => 'OpenXPKI::Server::Workflow::Persister::Archiver',
99        %{ $config->{persister} // {} },
100    };
101
102    subtest $testname => sub {
103        my $workflow_type = "TESTWORKFLOW".int(rand(2**32));
104
105        #
106        # Setup test context
107        #
108        my $oxitest = OpenXPKI::Test->new(
109            with => [ qw( TestRealms ) ],
110            also_init => "workflow_factory",
111            add_config => {
112                "realm.alpha.workflow.persister.Archiver" => $persister,
113                "realm.alpha.workflow.def.$workflow_type" => $wf_def,
114                "realm.alpha.workflow.def.$workflow_type.field" => $fields,
115                "realm.alpha.workflow.def.$workflow_type.attribute" => $attributes,
116                "realm.alpha.workflow.def.$workflow_type.state.SUCCESS.output" => ($config->{workflow}->{success_output} // []),
117                "realm.alpha.workflow.def.$workflow_type.state.FAILURE.output" => ($config->{workflow}->{failure_output} // []),
118            },
119        );
120
121        $oxitest->session->data->role("MyFairyKing");
122
123        my $wf1;
124
125        # Create workflow
126        lives_and {
127            $wf1 = CTX('workflow_factory')->get_factory->create_workflow($workflow_type);
128            ok ref $wf1;
129        } "create test workflow" or die("Could not create workflow");
130
131        # Run workflow action to
132        #  - set fields and attributes and
133        #  - trigger cleanup (only if $fail == 0)
134        lives_ok {
135            $wf1->execute_action("testwf_noop");
136        } "execute workflow action";
137
138        if ($force_failure) {
139            note "manually failing workflow";
140            $wf1->set_failed('entangled something', 'we just saw this...');
141        }
142        else {
143            lives_ok {
144                $wf1->execute_action("testwf_noop");
145            } "execute workflow action";
146        }
147
148        my $wf2;
149        lives_and {
150            $wf2 = CTX('workflow_factory')->get_factory->fetch_workflow($workflow_type, $wf1->id);
151            ok ref $wf2;
152        } "refetch workflow from database";
153
154        cmp_deeply $wf2->context->param, {
155            %{ $items->{field} },
156            creator => ignore(),
157            workflow_id => ignore()
158        }, "correct context items";
159
160        cmp_deeply $wf2->attrib, { %{$items->{attribute}}, creator => 'dummy' },
161            "correct attributes";
162
163        my $history = [ map { $_->action } $wf2->get_history ];
164        cmp_bag $history, $items->{history},
165            "correct history"
166                or diag(join "", map { sprintf "[%s] %s --> ", $_->state, $_->action  } sort { $a->date->epoch <=> $b->date->epoch } $wf2->get_history);
167
168        $oxitest->dbi->delete_and_commit(from => 'workflow', where => { workflow_type => $workflow_type });
169        $oxitest->dbi->delete_and_commit(from => 'workflow_attributes', where => { workflow_id => $wf1->id });
170    };
171}
172
173#
174# Tests
175#
176
177# Input for each test:
178#
179# Fields:
180#   env: sky
181#   temp: -10           # part of the 'output' of state SUCCESS
182#   vehicle: plane      # explicitely configured as "cleanup: none"
183#
184# Attributes:
185#   color: blue
186#   hairstyle: bald
187#   shoesize: 10        # explicitely configured as "cleanup: none"
188
189# =============================================================================
190# Standard cleanup
191#
192items_ok "standard cleanup: internal defaults",
193    config => {
194        # Internal persister defaults:
195        #   field: finished
196        #   attribute: none
197        #   history: archived
198        workflow => {
199            field => { 'vehicle' => { cleanup => 'none' } },
200            attribute => { 'color' => { cleanup => 'finished' } },
201            success_output => [ 'temp' ],
202        },
203    },
204    expected => {
205        field => {
206            'vehicle' => 'plane',
207            'temp' => -10,
208        },
209        attribute => {
210            'shoesize'  => 10,
211            'hairstyle' => 'bald',
212        },
213        history => [ qw( testwf_noop testwf_set_context testwf_set_attr testwf_noop ) ],
214    };
215
216items_ok "standard cleanup: explicit persister settings", # ... merged with internal defaults
217    config => {
218        # Internal persister defaults:
219        #   field: finished
220        persister => {
221            cleanup_defaults => {
222                attribute => 'finished',
223                history => 'finished',
224            },
225        },
226        workflow => {
227            attribute => { 'shoesize' => { cleanup => 'none' } },
228        },
229    },
230    expected => {
231        field => { },
232        attribute => {
233            'shoesize' => 10,
234        },
235        history => [ qw( ) ],
236    };
237
238# =============================================================================
239# Cleanup upon forced failure
240#
241
242items_ok "forced failure: internal defaults",
243    force_failure => 1,
244    config => {
245        # Internal persister defaults:
246        #   field: keep
247        #   attribute: drop
248        #   history: keep
249        workflow => {
250            field => { 'temp' => { onfail => 'drop' } },
251            attribute => { 'shoesize' => { onfail => 'keep' } },
252        },
253    },
254    expected => {
255        field => {
256            'env' => 'sky',
257            'vehicle' => 'plane',
258        },
259        attribute => {
260            'shoesize' => 10,
261        },
262        # duplicate 'testwf_set_attr' as state FAILURE also logs it as action
263        history => [ qw( testwf_noop testwf_set_context testwf_set_attr testwf_set_attr ) ],
264   };
265
266items_ok "forced failure: explicit persister settings",
267    force_failure => 1,
268    config => {
269        # Internal persister defaults:
270        #   attribute: drop
271        persister => {
272            onfail_defaults => {
273                field => 'drop',
274                history => 'drop',
275            },
276        },
277        workflow => {
278            field => { 'env' => { onfail => 'keep' } },
279            failure_output => [ 'vehicle' ],
280        },
281    },
282    expected => {
283        field => {
284            'env' => 'sky',
285            'vehicle' => 'plane',
286        },
287        attribute => { },
288        # state FAILURE also logs 'testwf_set_attr' as action
289        history => [ qw( testwf_set_attr ) ],
290   };
291