Project

General

Profile

Download (11.6 KB) Statistics
| Branch: | Tag: | Revision:
1
/**
2
* Copyright (C) 2021 EDIT
3
* European Distributed Institute of Taxonomy
4
* http://www.e-taxonomy.eu
5
*
6
* The contents of this file are subject to the Mozilla Public License Version 1.1
7
* See LICENSE.TXT at the top of this package for the full license terms.
8
*/
9
package eu.etaxonomy.cdm.api.service.security;
10

    
11
import static org.junit.Assert.assertEquals;
12
import static org.junit.Assert.assertFalse;
13
import static org.junit.Assert.assertNotNull;
14
import static org.junit.Assert.assertTrue;
15

    
16
import java.io.FileNotFoundException;
17
import java.time.Duration;
18
import java.util.concurrent.CountDownLatch;
19
import java.util.regex.Matcher;
20
import java.util.regex.Pattern;
21

    
22
import javax.mail.internet.MimeMessage;
23

    
24
import org.apache.log4j.Level;
25
import org.apache.log4j.Logger;
26
import org.junit.After;
27
import org.junit.Before;
28
import org.junit.Test;
29
import org.springframework.beans.factory.annotation.Autowired;
30
import org.springframework.core.env.Environment;
31
import org.springframework.mail.javamail.JavaMailSender;
32
import org.springframework.util.concurrent.ListenableFuture;
33
import org.subethamail.wiser.Wiser;
34
import org.subethamail.wiser.WiserMessage;
35
import org.unitils.dbunit.annotation.DataSet;
36
import org.unitils.spring.annotation.SpringBeanByType;
37

    
38
import eu.etaxonomy.cdm.api.security.IPasswordResetTokenStore;
39
import eu.etaxonomy.cdm.api.security.PasswordResetRequest;
40
import eu.etaxonomy.cdm.api.security.PasswordResetTokenStore;
41
import eu.etaxonomy.cdm.api.service.IUserService;
42
import eu.etaxonomy.cdm.model.permission.User;
43
import eu.etaxonomy.cdm.test.unitils.CleanSweepInsertLoadStrategy;
44

    
45
/**
46
 * @author a.kohlbecker
47
 * @since Nov 8, 2021
48
 */
49
public class PasswordResetServiceTest extends eu.etaxonomy.cdm.test.integration.CdmTransactionalIntegrationTest {
50

    
51
    /**
52
     *
53
     */
54
    private static final double maxRequestRate = 4.0;
55

    
56
    Logger logger = Logger.getLogger(PasswordResetServiceTest.class);
57

    
58
    private static final int rateLimiterTimeout = 200;
59
    private static final String userName = "pwdResetTestUser";
60
    private static final String userPWD = "super_SECURE_123";
61
    private static final String newPWD = "NEW_123_new_456";
62
    private static final String userEmail = "pwdResetTestUser@cybertaxonomy.test";
63

    
64

    
65
    private static String base64UrlSaveCharClass = "[a-zA-Z0-9\\-_]";
66

    
67

    
68
    private static final String requestFormUrlTemplate = "http://cybertaxonomy.test/passwordReset?userName={%s}&sessID=f8d8sf8dsf";
69

    
70
    @SpringBeanByType
71
    private IUserService userService;
72

    
73
    @SpringBeanByType
74
    private IPasswordResetService passwordResetService;
75

    
76
    @SpringBeanByType
77
    private IPasswordResetTokenStore passwordResetTokenStore;
78

    
79
    @SpringBeanByType
80
    private JavaMailSender emailSender;
81

    
82
    @Autowired
83
    private Environment env;
84

    
85
    private Wiser wiser = null;
86

    
87
    private CountDownLatch resetTokenSendSignal;
88
    private CountDownLatch resetTokenSendSignal2;
89
    private CountDownLatch passwordChangedSignal;
90
    Throwable assyncError = null;
91

    
92
    @Before
93
    public void startEmailServer() {
94
        // Integer smtpPort = env.getProperty(SendEmailConfigurer.PORT, Integer.class);
95
        wiser = new Wiser();
96
        wiser.setPort(2500); // must be the same as configured for SendEmailConfigurer.PORT
97
        wiser.start();
98
        logger.debug("Wiser email server started");
99
    }
100

    
101

    
102
    @Before
103
    public void createUser() {
104
        User user = User.NewInstance(userName, userPWD);
105
        user.setEmailAddress(userEmail);
106
        userService.save(user);
107
        commitAndStartNewTransaction();
108
        // printDataSet(System.err, "User");
109
    }
110

    
111
    @Before
112
    public void resetpasswordResetService() throws InterruptedException {
113
        logger.setLevel(Level.DEBUG);
114
        Logger.getLogger(PasswordResetRequest.class).setLevel(Level.TRACE);
115
        // speed up testing
116
        passwordResetService.setRateLimiterTimeout(Duration.ofMillis(rateLimiterTimeout));
117
        passwordResetService.setRate(maxRequestRate);
118
        // pause long enough to avoid conflicts
119
        long sleepTime = Math.round(1000 / maxRequestRate) + rateLimiterTimeout;
120
        Thread.sleep(sleepTime);
121
    }
122

    
123
    @Before
124
    public void resetAsyncVars() {
125
        assyncError = null;
126
        resetTokenSendSignal = null;
127
        resetTokenSendSignal2 = null;
128
        passwordChangedSignal = null;
129
    }
130

    
131
    @After
132
    public void removeUser() {
133
        userService.deleteUser(userName);
134
        userService.getSession().flush();
135
        commitAndStartNewTransaction();
136
    }
137

    
138
    @After
139
    public void stopEmailServer() {
140
        wiser.stop();
141
    }
142

    
143
    @Test
144
    @DataSet(loadStrategy = CleanSweepInsertLoadStrategy.class, value="/eu/etaxonomy/cdm/database/ClearDBDataSet.xml")
145
    public void testSuccessfulEmailReset() throws Throwable {
146

    
147
        logger.debug("testSuccessfulEmailReset() ...");
148

    
149
        // printDataSet(System.err, "UserAccount");
150

    
151
        resetTokenSendSignal = new CountDownLatch(1);
152
        passwordChangedSignal = new CountDownLatch(1);
153

    
154
        ListenableFuture<Boolean> emailResetFuture = passwordResetService.emailResetToken(userName, requestFormUrlTemplate);
155
        emailResetFuture.addCallback(
156
                requestSuccessVal -> {
157
                    resetTokenSendSignal.countDown();
158
                }, futureException -> {
159
                    assyncError = futureException;
160
                    resetTokenSendSignal.countDown();
161
                });
162

    
163
        // -- wait for passwordResetService.emailResetToken() to complete
164
        resetTokenSendSignal.await();
165

    
166
        if(assyncError != null) {
167
            throw assyncError;
168
        }
169

    
170
        assertNotNull(emailResetFuture.get());
171
        assertEquals(1, wiser.getMessages().size());
172

    
173
        // -- read email message
174
        WiserMessage requestMessage = wiser.getMessages().get(0);
175
        MimeMessage requestMimeMessage = requestMessage.getMimeMessage();
176
        assertEquals(PasswordResetTemplates.RESET_REQUEST_EMAIL_SUBJECT_TEMPLATE.replace("${userName}", userName), requestMimeMessage.getSubject());
177

    
178
        String messageContent = requestMimeMessage.getContent().toString();
179
        // -- extract token
180
        Pattern pattern = Pattern.compile("=\\{(" + base64UrlSaveCharClass + "+)\\}");
181
        Matcher m = pattern.matcher(messageContent);
182
        assertTrue(m.find());
183
        assertEquals(PasswordResetTokenStore.TOKEN_LENGTH + 17, m.group(1).length());
184

    
185
        // -- change password
186
        ListenableFuture<Boolean> resetPasswordFuture = passwordResetService.resetPassword( m.group(1), newPWD);
187
        resetPasswordFuture.addCallback(requestSuccessVal -> {
188
            passwordChangedSignal.countDown();
189
        }, futureException -> {
190
            assyncError =  futureException;
191
            passwordChangedSignal.countDown();
192
        });
193
        // -- wait for passwordResetService.resetPassword to complete
194
        passwordChangedSignal.await();
195

    
196
        assertTrue(resetPasswordFuture.get());
197
        assertEquals(2, wiser.getMessages().size());
198
        WiserMessage successMessage = wiser.getMessages().get(1);
199
        MimeMessage successMimeMessage = successMessage.getMimeMessage();
200
        assertEquals(PasswordResetTemplates.RESET_SUCCESS_EMAIL_SUBJECT_TEMPLATE.replace("${userName}", userName), successMimeMessage.getSubject());
201
    }
202

    
203
    @Test
204
    @DataSet(loadStrategy = CleanSweepInsertLoadStrategy.class, value="/eu/etaxonomy/cdm/database/ClearDBDataSet.xml")
205
    public void emailResetTokenTimeoutTest() throws Throwable {
206

    
207
        // Logger.getLogger(PasswordResetRequest.class).setLevel(Level.TRACE);
208

    
209
        resetTokenSendSignal = new CountDownLatch(1);
210
        resetTokenSendSignal2 = new CountDownLatch(1);
211

    
212
        passwordResetService.setRate(0.1);
213
        passwordResetService.setRateLimiterTimeout(Duration.ofMillis(1)); // as should as possible to allow the fist call to be successful
214

    
215
        logger.debug("1. request");
216
        ListenableFuture<Boolean> emailResetFuture = passwordResetService.emailResetToken(userName, requestFormUrlTemplate);
217
        emailResetFuture.addCallback(
218
                requestSuccessVal -> {
219
                    logger.debug("success 1");
220
                    resetTokenSendSignal.countDown();
221
                }, futureException -> {
222
                    logger.debug("error 1");
223
                    assyncError = futureException;
224
                    resetTokenSendSignal.countDown();
225
                });
226

    
227
        logger.debug("2. request");
228
        ListenableFuture<Boolean> emailResetFuture2 = passwordResetService.emailResetToken(userName, requestFormUrlTemplate);
229
        emailResetFuture2.addCallback(
230
                requestSuccessVal -> {
231
                    logger.debug("success 2");
232
                    resetTokenSendSignal2.countDown();
233
                }, futureException -> {
234
                    logger.debug("error 2");
235
                    assyncError = futureException;
236
                    resetTokenSendSignal2.countDown();
237
                });
238

    
239
        // -- wait for passwordResetService.emailResetToken() to complete
240
        resetTokenSendSignal.await();
241
        resetTokenSendSignal2.await();
242

    
243
        logger.debug("all completed, testing assertions");
244

    
245
        if(assyncError != null) {
246
            throw assyncError; // an error should not have been thrown
247
        }
248
        assertTrue("First request should have been successful", emailResetFuture.get());
249
        assertFalse("Second request should have been rejecded", emailResetFuture2.get());
250
    }
251

    
252
    @Test
253
    @DataSet(loadStrategy = CleanSweepInsertLoadStrategy.class, value="/eu/etaxonomy/cdm/database/ClearDBDataSet.xml")
254
    public void emailResetToken_ivalidUserNameTest() throws Throwable {
255

    
256
        logger.debug("emailResetToken_ivalidUserNameTest() ...");
257

    
258
        resetTokenSendSignal = new CountDownLatch(1);
259

    
260
        passwordResetService.setRateLimiterTimeout(Duration.ofMillis(1)); // as should as possible to allow the fist call to be successful (with 1ns the fist call fails!)
261
        ListenableFuture<Boolean> emailResetFuture = passwordResetService.emailResetToken("iDoNotExist", requestFormUrlTemplate);
262
        emailResetFuture.addCallback(
263
                requestSuccessVal -> {
264
                    resetTokenSendSignal.countDown();
265
                }, futureException -> {
266
                    assyncError = futureException;
267
                    resetTokenSendSignal.countDown();
268
                });
269

    
270

    
271
        // -- wait for passwordResetService.emailResetToken() to complete
272
        resetTokenSendSignal.await();
273

    
274
        if(assyncError != null) {
275
            throw assyncError; // emailResetToken must be agnostic of the existence of user names
276
        }
277
        assertTrue("The request should look like succesful even in the user does not exist.", emailResetFuture.get());
278
    }
279

    
280
    @Test
281
    @DataSet(loadStrategy = CleanSweepInsertLoadStrategy.class, value="/eu/etaxonomy/cdm/database/ClearDBDataSet.xml")
282
    public void testInvalidToken() throws Throwable {
283

    
284
        logger.debug("testInvalidToken() ...");
285

    
286
        passwordChangedSignal = new CountDownLatch(1);
287

    
288
        // -- change password
289
        ListenableFuture<Boolean> resetPasswordFuture = passwordResetService.resetPassword( "IUER9843URIO--INVALID-TOKEN--UWEUR89EUWWEOIR", newPWD);
290
        resetPasswordFuture.addCallback(requestSuccessVal -> {
291
            passwordChangedSignal.countDown();
292
        }, futureException -> {
293
            assyncError =  futureException;
294
            passwordChangedSignal.countDown();
295
        });
296
        // -- wait for passwordResetService.resetPassword to complete
297
        passwordChangedSignal.await();
298

    
299
        assertNotNull(assyncError);
300
        assertEquals(PasswordResetException.class, assyncError.getClass());
301
        assertEquals(0, wiser.getMessages().size());
302
    }
303

    
304
    @Override
305
    public void createTestDataSet() throws FileNotFoundException {
306
        // not needed
307
    }
308

    
309
}
    (1-1/1)