View Javadoc
1   /*
2    * Copyright (c) 2002-2025, City of Paris
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met:
8    *
9    *  1. Redistributions of source code must retain the above copyright notice
10   *     and the following disclaimer.
11   *
12   *  2. Redistributions in binary form must reproduce the above copyright notice
13   *     and the following disclaimer in the documentation and/or other materials
14   *     provided with the distribution.
15   *
16   *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
17   *     contributors may be used to endorse or promote products derived from
18   *     this software without specific prior written permission.
19   *
20   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
24   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30   * POSSIBILITY OF SUCH DAMAGE.
31   *
32   * License 1.0
33   */
34  package fr.paris.lutece.portal.web.system;
35  
36  import java.io.File;
37  import java.io.FileInputStream;
38  import java.io.FileNotFoundException;
39  import java.io.FileOutputStream;
40  import java.io.IOException;
41  import java.util.Properties;
42  import java.util.concurrent.BrokenBarrierException;
43  import java.util.concurrent.TimeUnit;
44  import java.util.concurrent.TimeoutException;
45  
46  import org.springframework.mock.web.MockHttpServletRequest;
47  
48  import fr.paris.lutece.portal.business.user.AdminUser;
49  import fr.paris.lutece.portal.service.admin.AccessDeniedException;
50  import fr.paris.lutece.portal.service.admin.PasswordResetException;
51  import fr.paris.lutece.portal.service.daemon.AppDaemonService;
52  import fr.paris.lutece.portal.service.daemon.DaemonEntry;
53  import fr.paris.lutece.portal.service.daemon.TestDaemon;
54  import fr.paris.lutece.portal.service.datastore.DatastoreService;
55  import fr.paris.lutece.portal.service.security.SecurityTokenService;
56  import fr.paris.lutece.portal.service.util.AppPropertiesService;
57  import fr.paris.lutece.portal.service.util.AppLogService;
58  import fr.paris.lutece.test.LuteceTestCase;
59  import fr.paris.lutece.test.Utils;
60  
61  public class DaemonsJspBeanTest extends LuteceTestCase
62  {
63      private static final String TEMPLATE_MANAGE_DAEMONS = "admin/system/manage_daemons.html";
64      private static final String JUNIT_DAEMON = "JUNIT";
65      private static final String DAEMON_INTERVAL_DSKEY = "core.daemon." + JUNIT_DAEMON + ".interval";
66      private DaemonsJspBean bean;
67      private DaemonEntry _entry;
68      private String origMaxInitialStartDelay;
69      private TestDaemon _testDaemon;
70  
71      @Override
72      protected void setUp( ) throws Exception
73      {
74          super.setUp( );
75          origMaxInitialStartDelay = setInitialStartDelay( );
76          assertEquals( "Failed to adjust daemon initial start delay", 1, AppPropertiesService.getPropertyInt( "daemon.maxInitialStartDelay", 3000 ) );
77          bean = new DaemonsJspBean( );
78          _entry = new DaemonEntry( );
79          _entry.setId( JUNIT_DAEMON );
80          _entry.setNameKey( JUNIT_DAEMON );
81          _entry.setDescriptionKey( JUNIT_DAEMON );
82          _entry.setClassName( TestDaemon.class.getName( ) );
83          _entry.setPluginName( "core" );
84          // AppDaemonService.registerDaemon will copy this datastore value in the entry.
85          DatastoreService.setInstanceDataValue( DAEMON_INTERVAL_DSKEY, "1" );
86          AppDaemonService.registerDaemon( _entry );
87          _testDaemon = (TestDaemon) AppDaemonService.getDaemon( JUNIT_DAEMON );
88      }
89  
90      private String setInitialStartDelay( ) throws FileNotFoundException, IOException
91      {
92          File propertiesFile = new File( getResourcesDir( ) + "/WEB-INF/conf/daemons.properties" );
93          Properties props = new Properties( );
94          try ( FileInputStream is = new FileInputStream( propertiesFile ) )
95          {
96              props.load( is );
97          }
98          String orig = props.getProperty( "daemon.maxInitialStartDelay" );
99          props.setProperty( "daemon.maxInitialStartDelay", "1" );
100         try ( FileOutputStream out = new FileOutputStream( propertiesFile ) )
101         {
102             props.store( out, "junit" );
103         }
104         AppPropertiesService.reloadAll( );
105         return orig;
106     }
107 
108     @Override
109     protected void tearDown( ) throws Exception
110     {
111         // Attempt to unlock test daemon threads that would be blocked on the barriers,
112         // for
113         // example if the test throws an unexpected exception, causing it to not call
114         // go() and waitforcompletion() to unlock the barriers.
115         // A stuck thread would make the next test start with the daemon already running
116         // (even though it is unregistered, because the logic to remove running daemons
117         // is done at the end of the thread),
118         // so the calls the go() and waitcompletion() could be imbalanced. And it's
119         // highly
120         // confusing that the tests keep this kind of state between them.
121         // Note that the thread would die off eventually because the barriers have a
122         // timeout, but tests
123         // continue to execute in the mean time and fail with obscure reasons.
124         _testDaemon.resetGo( );
125         _testDaemon.resetCompletion( );
126 
127         DatastoreService.removeInstanceData( DAEMON_INTERVAL_DSKEY );
128         AppDaemonService.stopDaemon( JUNIT_DAEMON );
129         AppDaemonService.unregisterDaemon( JUNIT_DAEMON );
130         restoreInitialStartDelay( origMaxInitialStartDelay );
131         assertEquals( "Failed to restore daemon initial start delay", origMaxInitialStartDelay,
132                 AppPropertiesService.getProperty( "daemon.maxInitialStartDelay" ) );
133         super.tearDown( );
134     }
135 
136     private void restoreInitialStartDelay( String orig ) throws FileNotFoundException, IOException
137     {
138         File propertiesFile = new File( getResourcesDir( ) + "/WEB-INF/conf/daemons.properties" );
139         Properties props = new Properties( );
140         try ( FileInputStream is = new FileInputStream( propertiesFile ) )
141         {
142             props.load( is );
143         }
144         if ( orig == null )
145         {
146             props.remove( "daemon.maxInitialStartDelay" );
147         }
148         else
149         {
150             props.setProperty( "daemon.maxInitialStartDelay", orig );
151         }
152         try ( FileOutputStream out = new FileOutputStream( propertiesFile ) )
153         {
154             props.store( out, "junit" );
155         }
156         AppPropertiesService.reloadAll( );
157     }
158 
159     public void testDoDaemonActionStart( ) throws InterruptedException, BrokenBarrierException, TimeoutException, AccessDeniedException
160     {
161         long lReadyTime;
162         long lScheduledTime;
163         assertFalse( _entry.isRunning( ) );
164         MockHttpServletRequest request = new MockHttpServletRequest( );
165         request.setParameter( "action", "START" );
166         request.setParameter( "daemon", JUNIT_DAEMON );
167         request.setParameter( SecurityTokenService.PARAMETER_TOKEN, SecurityTokenService.getInstance( ).getToken( request, TEMPLATE_MANAGE_DAEMONS ) );
168         lReadyTime = System.nanoTime( );
169         bean.doDaemonAction( request ); // Daemon should run periodically with interval of 1s
170 
171         assertTrue( _entry.isRunning( ) );
172         _testDaemon.go( );
173         lScheduledTime = System.nanoTime( );
174         AppLogService.info( "Daemon scheduled after {} ms", ( ( lScheduledTime - lReadyTime ) / 1000000 ) );
175 
176         _testDaemon.waitForCompletion( ); // Complete first run without a timeout
177 
178         lReadyTime = System.nanoTime( );
179         _testDaemon.go( );
180         lScheduledTime = System.nanoTime( );
181         long lRescheduleDurationMilliseconds = ( ( lScheduledTime - lReadyTime ) / 1000000 );
182         long lMarginMilliseconds = 250;
183         AppLogService.info( "Daemon scheduled after {} ms", lRescheduleDurationMilliseconds );
184 
185         _testDaemon.waitForCompletion( ); // Complete second run without a timeout. More runs would follow if we
186                                           // continued
187 
188         assertTrue( "Daemon should be re-scheduled approximately 1 second after the end of the previous run, but got " + lRescheduleDurationMilliseconds + "ms",
189                 1000 - lMarginMilliseconds <= lRescheduleDurationMilliseconds && lRescheduleDurationMilliseconds <= 1000 + lMarginMilliseconds );
190     }
191 
192     public void testDoDaemonActionStartInvalidToken( ) throws InterruptedException, BrokenBarrierException, TimeoutException, AccessDeniedException
193     {
194         assertFalse( _entry.isRunning( ) );
195         MockHttpServletRequest request = new MockHttpServletRequest( );
196         request.setParameter( "action", "START" );
197         request.setParameter( "daemon", JUNIT_DAEMON );
198         request.setParameter( SecurityTokenService.PARAMETER_TOKEN, SecurityTokenService.getInstance( ).getToken( request, TEMPLATE_MANAGE_DAEMONS ) + "b" );
199         try
200         {
201             bean.doDaemonAction( request ); // Daemon should run periodically with interval of 1s
202             fail( "Should have thrown" );
203         }
204         catch( AccessDeniedException e )
205         {
206             assertFalse( _entry.isRunning( ) );
207             try
208             {
209                 // Here the daemon should not be launched
210                 _testDaemon.go( 2500, TimeUnit.MILLISECONDS );
211                 fail( "Daemon running be should not" );
212             }
213             catch( TimeoutException te )
214             {
215                 // ok
216             }
217         }
218     }
219 
220     public void testDoDaemonActionStartNoToken( ) throws InterruptedException, BrokenBarrierException, TimeoutException, AccessDeniedException
221     {
222         assertFalse( _entry.isRunning( ) );
223         MockHttpServletRequest request = new MockHttpServletRequest( );
224         request.setParameter( "action", "START" );
225         request.setParameter( "daemon", JUNIT_DAEMON );
226         try
227         {
228             bean.doDaemonAction( request ); // Daemon should run periodically with interval of 1s
229             fail( "Should have thrown" );
230         }
231         catch( AccessDeniedException e )
232         {
233             assertFalse( _entry.isRunning( ) );
234             try
235             {
236                 // Here the daemon should not be launched
237                 _testDaemon.go( 2500, TimeUnit.MILLISECONDS );
238                 fail( "Daemon running be should not" );
239             }
240             catch( TimeoutException te )
241             {
242                 // ok
243             }
244         }
245     }
246 
247     public void testDoDaemonActionStop( ) throws InterruptedException, BrokenBarrierException, TimeoutException, AccessDeniedException
248     {
249         assertFalse( _entry.isRunning( ) );
250         AppDaemonService.startDaemon( JUNIT_DAEMON ); // Daemon should run
251                                                       // periodically with
252                                                       // interval of 1s
253         _testDaemon.go( );
254         _testDaemon.waitForCompletion( );
255         // We have about 1 second to stop the daemon
256         MockHttpServletRequest request = new MockHttpServletRequest( );
257         request.setParameter( "action", "STOP" );
258         request.setParameter( "daemon", JUNIT_DAEMON );
259         request.setParameter( SecurityTokenService.PARAMETER_TOKEN, SecurityTokenService.getInstance( ).getToken( request, TEMPLATE_MANAGE_DAEMONS ) );
260         bean.doDaemonAction( request );
261         assertFalse( _entry.isRunning( ) );
262         try
263         {
264             // Here the daemon should not be relaunched after a 1s interval. So
265             // wait 2.5 seconds until a timeout.
266             _testDaemon.go( 2500, TimeUnit.MILLISECONDS );
267             fail( "Daemon still running after stop" );
268         }
269         catch( TimeoutException e )
270         {
271             // ok
272         }
273     }
274 
275     public void testDoDaemonActionStopInvalidToken( ) throws InterruptedException, BrokenBarrierException, TimeoutException, AccessDeniedException
276     {
277         assertFalse( _entry.isRunning( ) );
278         AppDaemonService.startDaemon( JUNIT_DAEMON ); // Daemon should run
279                                                       // periodically with
280                                                       // interval of 1s
281         _testDaemon.go( );
282         _testDaemon.waitForCompletion( );
283         // We have about 1 second to stop the daemon
284         MockHttpServletRequest request = new MockHttpServletRequest( );
285         request.setParameter( "action", "STOP" );
286         request.setParameter( "daemon", JUNIT_DAEMON );
287         request.setParameter( SecurityTokenService.PARAMETER_TOKEN, SecurityTokenService.getInstance( ).getToken( request, TEMPLATE_MANAGE_DAEMONS ) + "b" );
288         try
289         {
290             bean.doDaemonAction( request );
291             fail( "Should have thrown" );
292         }
293         catch( AccessDeniedException e )
294         {
295             assertTrue( _entry.isRunning( ) );
296             _testDaemon.go( );
297             _testDaemon.waitForCompletion( );
298         }
299     }
300 
301     public void testDoDaemonActionStopNoToken( ) throws InterruptedException, BrokenBarrierException, TimeoutException, AccessDeniedException
302     {
303         assertFalse( _entry.isRunning( ) );
304         AppDaemonService.startDaemon( JUNIT_DAEMON ); // Daemon should run
305                                                       // periodically with
306                                                       // interval of 1s
307         _testDaemon.go( );
308         _testDaemon.waitForCompletion( );
309         // We have about 1 second to stop the daemon
310         MockHttpServletRequest request = new MockHttpServletRequest( );
311         request.setParameter( "action", "STOP" );
312         request.setParameter( "daemon", JUNIT_DAEMON );
313         try
314         {
315             bean.doDaemonAction( request );
316             fail( "Should have thrown" );
317         }
318         catch( AccessDeniedException e )
319         {
320             assertTrue( _entry.isRunning( ) );
321             _testDaemon.go( );
322             _testDaemon.waitForCompletion( );
323         }
324     }
325 
326     public void testDoDaemonActionRun( ) throws InterruptedException, BrokenBarrierException, TimeoutException, AccessDeniedException
327     {
328         long lReadyTime;
329         long lScheduledTime;
330         assertFalse( _entry.isRunning( ) );
331         _entry.setInterval( 1000 );
332 
333         lReadyTime = System.nanoTime( );
334         AppDaemonService.startDaemon( JUNIT_DAEMON ); // Daemon should run
335                                                       // periodically with
336                                                       // interval of 1000s
337         _testDaemon.go( );
338         lScheduledTime = System.nanoTime( );
339         AppLogService.info( "Daemon scheduled after {} ms", ( ( lScheduledTime - lReadyTime ) / 1000000 ) );
340 
341         _testDaemon.waitForCompletion( );
342 
343         MockHttpServletRequest request = new MockHttpServletRequest( );
344         request.setParameter( "action", "RUN" ); // Manually do 1 run of the
345                                                  // daemon now
346         request.setParameter( "daemon", JUNIT_DAEMON );
347         request.setParameter( SecurityTokenService.PARAMETER_TOKEN, SecurityTokenService.getInstance( ).getToken( request, TEMPLATE_MANAGE_DAEMONS ) );
348         lReadyTime = System.nanoTime( );
349         bean.doDaemonAction( request );
350 
351         _testDaemon.go( ); // It should run in less than 1000 seconds !
352         lScheduledTime = System.nanoTime( );
353         AppLogService.info( "Daemon scheduled after {} ms", ( ( lScheduledTime - lReadyTime ) / 1000000 ) );
354 
355         _testDaemon.waitForCompletion( );
356     }
357 
358     public void testDoDaemonActionRunInvalidToken( ) throws InterruptedException, BrokenBarrierException, TimeoutException, AccessDeniedException
359     {
360         long lReadyTime;
361         long lScheduledTime;
362         assertFalse( _entry.isRunning( ) );
363         _entry.setInterval( 1000 );
364 
365         lReadyTime = System.nanoTime( );
366         AppDaemonService.startDaemon( JUNIT_DAEMON ); // Daemon should run
367                                                       // periodically with
368                                                       // interval of 1000s
369         _testDaemon.go( );
370         lScheduledTime = System.nanoTime( );
371         AppLogService.info( "Daemon scheduled after {} ms", ( ( lScheduledTime - lReadyTime ) / 1000000 ) );
372 
373         _testDaemon.waitForCompletion( );
374 
375         MockHttpServletRequest request = new MockHttpServletRequest( );
376         request.setParameter( "action", "RUN" ); // Manually do 1 run of the
377                                                  // daemon now
378         request.setParameter( "daemon", JUNIT_DAEMON );
379         request.setParameter( SecurityTokenService.PARAMETER_TOKEN, SecurityTokenService.getInstance( ).getToken( request, TEMPLATE_MANAGE_DAEMONS ) + "b" );
380 
381         try
382         {
383             bean.doDaemonAction( request );
384             fail( "Should have thrown" );
385         }
386         catch( AccessDeniedException e )
387         {
388             try
389             {
390                 // Here the daemon should not be run
391                 _testDaemon.go( 1, TimeUnit.SECONDS );
392                 fail( "Daemon should not have run" );
393             }
394             catch( TimeoutException te )
395             {
396                 // ok
397             }
398         }
399     }
400 
401     public void testDoDaemonActionRunNoToken( ) throws InterruptedException, BrokenBarrierException, TimeoutException, AccessDeniedException
402     {
403         long lReadyTime;
404         long lScheduledTime;
405         assertFalse( _entry.isRunning( ) );
406         _entry.setInterval( 1000 );
407 
408         lReadyTime = System.nanoTime( );
409         AppDaemonService.startDaemon( JUNIT_DAEMON ); // Daemon should run
410                                                       // periodically with
411                                                       // interval of 1000s
412         _testDaemon.go( );
413         lScheduledTime = System.nanoTime( );
414         AppLogService.info( "Daemon scheduled after {} ms", ( ( lScheduledTime - lReadyTime ) / 1000000 ) );
415 
416         _testDaemon.waitForCompletion( );
417 
418         MockHttpServletRequest request = new MockHttpServletRequest( );
419         request.setParameter( "action", "RUN" ); // Manually do 1 run of the
420                                                  // daemon now
421         request.setParameter( "daemon", JUNIT_DAEMON );
422 
423         try
424         {
425             bean.doDaemonAction( request );
426             fail( "Should have thrown" );
427         }
428         catch( AccessDeniedException e )
429         {
430             try
431             {
432                 // Here the daemon should not be run
433                 _testDaemon.go( 1, TimeUnit.SECONDS );
434                 fail( "Daemon should not have run" );
435             }
436             catch( TimeoutException te )
437             {
438                 // ok
439             }
440         }
441     }
442 
443     public void testDoDaemonActionUpdateInterval( ) throws AccessDeniedException
444     {
445         final long lTestInterval = 314159L;
446         MockHttpServletRequest request = new MockHttpServletRequest( );
447         request.setParameter( "action", "UPDATE_INTERVAL" );
448         request.setParameter( "daemon", JUNIT_DAEMON );
449         request.setParameter( "interval", Long.toString( lTestInterval ) );
450         request.setParameter( SecurityTokenService.PARAMETER_TOKEN, SecurityTokenService.getInstance( ).getToken( request, TEMPLATE_MANAGE_DAEMONS ) );
451         bean.doDaemonAction( request );
452         assertEquals( lTestInterval, _entry.getInterval( ) );
453     }
454 
455     public void testDoDaemonActionUpdateIntervalInvalidToken( ) throws AccessDeniedException
456     {
457         final long lTestInterval = 314159L;
458         MockHttpServletRequest request = new MockHttpServletRequest( );
459         request.setParameter( "action", "UPDATE_INTERVAL" );
460         request.setParameter( "daemon", JUNIT_DAEMON );
461         request.setParameter( "interval", Long.toString( lTestInterval ) );
462         request.setParameter( SecurityTokenService.PARAMETER_TOKEN, SecurityTokenService.getInstance( ).getToken( request, TEMPLATE_MANAGE_DAEMONS ) + "b" );
463         try
464         {
465             bean.doDaemonAction( request );
466             fail( "Should have thrown" );
467         }
468         catch( AccessDeniedException e )
469         {
470             assertEquals( 1, _entry.getInterval( ) );
471         }
472     }
473 
474     public void testDoDaemonActionUpdateIntervalNoToken( ) throws AccessDeniedException
475     {
476         final long lTestInterval = 314159L;
477         MockHttpServletRequest request = new MockHttpServletRequest( );
478         request.setParameter( "action", "UPDATE_INTERVAL" );
479         request.setParameter( "daemon", JUNIT_DAEMON );
480         request.setParameter( "interval", Long.toString( lTestInterval ) );
481 
482         try
483         {
484             bean.doDaemonAction( request );
485             fail( "Should have thrown" );
486         }
487         catch( AccessDeniedException e )
488         {
489             assertEquals( 1, _entry.getInterval( ) );
490         }
491     }
492 
493     public void testDoDaemonActionUnknown( ) throws AccessDeniedException
494     {
495         try
496         {
497             MockHttpServletRequest request = new MockHttpServletRequest( );
498             request.setParameter( "action", "UNKNOWN" );
499             bean.doDaemonAction( request );
500             // does not throw
501         }
502         catch( Exception e )
503         {
504             fail( e.getMessage( ) );
505         }
506     }
507 
508     public void testGetManageDaemons( ) throws PasswordResetException, AccessDeniedException
509     {
510         MockHttpServletRequest request = new MockHttpServletRequest( );
511         Utils.registerAdminUserWithRigth( request, new AdminUser( ), DaemonsJspBean.RIGHT_DAEMONS_MANAGEMENT );
512         bean.init( request, DaemonsJspBean.RIGHT_DAEMONS_MANAGEMENT );
513         assertNotNull( bean.getManageDaemons( request ) );
514     }
515 }