Project

General

Profile

Download (11.5 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.mail.javamail.JavaMailSender;
30
import org.springframework.util.concurrent.ListenableFuture;
31
import org.subethamail.wiser.Wiser;
32
import org.subethamail.wiser.WiserMessage;
33
import org.unitils.dbunit.annotation.DataSet;
34
import org.unitils.spring.annotation.SpringBeanByName;
35
import org.unitils.spring.annotation.SpringBeanByType;
36

    
37
import eu.etaxonomy.cdm.api.security.AbstractRequestTokenStore;
38
import eu.etaxonomy.cdm.api.security.IAbstractRequestTokenStore;
39
import eu.etaxonomy.cdm.api.security.PasswordResetRequest;
40
import eu.etaxonomy.cdm.api.service.IUserService;
41
import eu.etaxonomy.cdm.model.permission.User;
42
import eu.etaxonomy.cdm.test.unitils.CleanSweepInsertLoadStrategy;
43

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

    
50
    private static final Logger logger = Logger.getLogger(PasswordResetServiceTest.class);
51

    
52
    private static final double maxRequestRate = 4.0;
53

    
54
    private static final int rateLimiterTimeout = 200;
55
    private static final String userName = "pwdResetTestUser";
56
    private static final String userPWD = "super_SECURE_123";
57
    private static final String newPWD = "NEW_123_new_456";
58
    private static final String userEmail = "pwdResetTestUser@cybertaxonomy.test";
59

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

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

    
64
    @SpringBeanByType
65
    private IUserService userService;
66

    
67
    @SpringBeanByType
68
    private IPasswordResetService passwordResetService;
69

    
70
    @SpringBeanByName
71
    private IAbstractRequestTokenStore<PasswordResetRequest> passwordResetTokenStore;
72

    
73
    @SpringBeanByType
74
    private JavaMailSender emailSender;
75

    
76
    private Wiser wiser = null;
77

    
78
    private CountDownLatch resetTokenSendSignal;
79
    private CountDownLatch resetTokenSendSignal2;
80
    private CountDownLatch passwordChangedSignal;
81
    Throwable assyncError = null;
82

    
83
    @Before
84
    public void startEmailServer() {
85
        // Integer smtpPort = env.getProperty(SendEmailConfigurer.PORT, Integer.class);
86
        wiser = new Wiser();
87
        wiser.setPort(2500); // must be the same as configured for SendEmailConfigurer.PORT
88
        wiser.start();
89
        logger.debug("Wiser email server started");
90
    }
91

    
92

    
93
    @Before
94
    public void createUser() {
95
        User user = User.NewInstance(userName, userPWD);
96
        user.setEmailAddress(userEmail);
97
        userService.save(user);
98
        commitAndStartNewTransaction();
99
        // printDataSet(System.err, "User");
100
    }
101

    
102
    @Before
103
    public void resetpasswordResetService() throws InterruptedException {
104
        logger.setLevel(Level.DEBUG);
105
        Logger.getLogger(PasswordResetRequest.class).setLevel(Level.TRACE);
106
        // speed up testing
107
        passwordResetService.setRateLimiterTimeout(Duration.ofMillis(rateLimiterTimeout));
108
        passwordResetService.setRate(maxRequestRate);
109
        // pause long enough to avoid conflicts
110
        long sleepTime = Math.round(1000 / maxRequestRate) + rateLimiterTimeout;
111
        Thread.sleep(sleepTime);
112
    }
113

    
114
    @Before
115
    public void resetAsyncVars() {
116
        assyncError = null;
117
        resetTokenSendSignal = null;
118
        resetTokenSendSignal2 = null;
119
        passwordChangedSignal = null;
120
    }
121

    
122
    @After
123
    public void removeUser() {
124
        userService.deleteUser(userName);
125
        userService.getSession().flush();
126
        commitAndStartNewTransaction();
127
    }
128

    
129
    @After
130
    public void stopEmailServer() {
131
        wiser.stop();
132
    }
133

    
134
    @Test
135
    @DataSet(loadStrategy = CleanSweepInsertLoadStrategy.class, value="/eu/etaxonomy/cdm/database/ClearDBDataSet.xml")
136
    public void testSuccessfulEmailReset() throws Throwable {
137

    
138
        logger.debug("testSuccessfulEmailReset() ...");
139

    
140
        // printDataSet(System.err, "UserAccount");
141

    
142
        resetTokenSendSignal = new CountDownLatch(1);
143
        passwordChangedSignal = new CountDownLatch(1);
144

    
145
        ListenableFuture<Boolean> emailResetFuture = passwordResetService.emailResetToken(userName, requestFormUrlTemplate);
146
        emailResetFuture.addCallback(
147
                requestSuccessVal -> {
148
                    resetTokenSendSignal.countDown();
149
                }, futureException -> {
150
                    assyncError = futureException;
151
                    resetTokenSendSignal.countDown();
152
                });
153

    
154
        // -- wait for passwordResetService.emailResetToken() to complete
155
        resetTokenSendSignal.await();
156

    
157
        if(assyncError != null) {
158
            throw assyncError;
159
        }
160

    
161
        assertNotNull(emailResetFuture.get());
162
        assertEquals(1, wiser.getMessages().size());
163

    
164
        // -- read email message
165
        WiserMessage requestMessage = wiser.getMessages().get(0);
166
        MimeMessage requestMimeMessage = requestMessage.getMimeMessage();
167
        assertEquals(UserAccountEmailTemplates.RESET_REQUEST_EMAIL_SUBJECT_TEMPLATE.replace("${userName}", userName), requestMimeMessage.getSubject());
168

    
169
        String messageContent = requestMimeMessage.getContent().toString();
170
        // -- extract token
171
        Pattern pattern = Pattern.compile("=\\{(" + base64UrlSaveCharClass + "+)\\}");
172
        Matcher m = pattern.matcher(messageContent);
173
        assertTrue(m.find());
174
        assertEquals(AbstractRequestTokenStore.TOKEN_LENGTH + 17, m.group(1).length());
175

    
176
        // -- change password
177
        ListenableFuture<Boolean> resetPasswordFuture = passwordResetService.resetPassword( m.group(1), newPWD);
178
        resetPasswordFuture.addCallback(requestSuccessVal -> {
179
            passwordChangedSignal.countDown();
180
        }, futureException -> {
181
            assyncError =  futureException;
182
            passwordChangedSignal.countDown();
183
        });
184
        // -- wait for passwordResetService.resetPassword to complete
185
        passwordChangedSignal.await();
186

    
187
        assertTrue(resetPasswordFuture.get());
188
        assertEquals(2, wiser.getMessages().size());
189
        WiserMessage successMessage = wiser.getMessages().get(1);
190
        MimeMessage successMimeMessage = successMessage.getMimeMessage();
191
        assertEquals(UserAccountEmailTemplates.RESET_SUCCESS_EMAIL_SUBJECT_TEMPLATE.replace("${userName}", userName), successMimeMessage.getSubject());
192
    }
193

    
194
    @Test
195
    @DataSet(loadStrategy = CleanSweepInsertLoadStrategy.class, value="/eu/etaxonomy/cdm/database/ClearDBDataSet.xml")
196
    public void emailResetTokenTimeoutTest() throws Throwable {
197

    
198
        // Logger.getLogger(PasswordResetRequest.class).setLevel(Level.TRACE);
199

    
200
        resetTokenSendSignal = new CountDownLatch(1);
201
        resetTokenSendSignal2 = new CountDownLatch(1);
202

    
203
        passwordResetService.setRate(0.1);
204
        passwordResetService.setRateLimiterTimeout(Duration.ofMillis(1)); // as should as possible to allow the fist call to be successful
205

    
206
        logger.debug("1. request");
207
        ListenableFuture<Boolean> emailResetFuture = passwordResetService.emailResetToken(userName, requestFormUrlTemplate);
208
        emailResetFuture.addCallback(
209
                requestSuccessVal -> {
210
                    logger.debug("success 1");
211
                    resetTokenSendSignal.countDown();
212
                }, futureException -> {
213
                    logger.debug("error 1");
214
                    assyncError = futureException;
215
                    resetTokenSendSignal.countDown();
216
                });
217

    
218
        logger.debug("2. request");
219
        ListenableFuture<Boolean> emailResetFuture2 = passwordResetService.emailResetToken(userName, requestFormUrlTemplate);
220
        emailResetFuture2.addCallback(
221
                requestSuccessVal -> {
222
                    logger.debug("success 2");
223
                    resetTokenSendSignal2.countDown();
224
                }, futureException -> {
225
                    logger.debug("error 2");
226
                    assyncError = futureException;
227
                    resetTokenSendSignal2.countDown();
228
                });
229

    
230
        // -- wait for passwordResetService.emailResetToken() to complete
231
        resetTokenSendSignal.await();
232
        resetTokenSendSignal2.await();
233

    
234
        logger.debug("all completed, testing assertions");
235

    
236
        if(assyncError != null) {
237
            throw assyncError; // an error should not have been thrown
238
        }
239
        assertTrue("First request should have been successful", emailResetFuture.get());
240
        assertFalse("Second request should have been rejecded", emailResetFuture2.get());
241
    }
242

    
243
    @Test
244
    @DataSet(loadStrategy = CleanSweepInsertLoadStrategy.class, value="/eu/etaxonomy/cdm/database/ClearDBDataSet.xml")
245
    public void emailResetToken_ivalidUserNameTest() throws Throwable {
246

    
247
        logger.debug("emailResetToken_ivalidUserNameTest() ...");
248

    
249
        resetTokenSendSignal = new CountDownLatch(1);
250

    
251
        passwordResetService.setRateLimiterTimeout(Duration.ofMillis(1)); // as should as possible to allow the fist call to be successful (with 1ns the fist call fails!)
252
        ListenableFuture<Boolean> emailResetFuture = passwordResetService.emailResetToken("iDoNotExist", requestFormUrlTemplate);
253
        emailResetFuture.addCallback(
254
                requestSuccessVal -> {
255
                    resetTokenSendSignal.countDown();
256
                }, futureException -> {
257
                    assyncError = futureException;
258
                    resetTokenSendSignal.countDown();
259
                });
260

    
261
        // -- wait for passwordResetService.emailResetToken() to complete
262
        resetTokenSendSignal.await();
263

    
264
        if(assyncError != null) {
265
            throw assyncError; // emailResetToken must be agnostic of the existence of user names
266
        }
267
        assertTrue("The request should look like succesful even in the user does not exist.", emailResetFuture.get());
268
    }
269

    
270
    @Test
271
    @DataSet(loadStrategy = CleanSweepInsertLoadStrategy.class, value="/eu/etaxonomy/cdm/database/ClearDBDataSet.xml")
272
    public void testInvalidToken() throws Throwable {
273

    
274
        logger.debug("testInvalidToken() ...");
275

    
276
        passwordChangedSignal = new CountDownLatch(1);
277

    
278
        // -- change password
279
        ListenableFuture<Boolean> resetPasswordFuture = passwordResetService.resetPassword( "IUER9843URIO--INVALID-TOKEN--UWEUR89EUWWEOIR", newPWD);
280
        resetPasswordFuture.addCallback(requestSuccessVal -> {
281
            passwordChangedSignal.countDown();
282
        }, futureException -> {
283
            assyncError =  futureException;
284
            passwordChangedSignal.countDown();
285
        });
286
        // -- wait for passwordResetService.resetPassword to complete
287
        passwordChangedSignal.await();
288

    
289
        assertNotNull(assyncError);
290
        assertEquals(AccountSelfManagementException.class, assyncError.getClass());
291
        assertEquals(0, wiser.getMessages().size());
292
    }
293

    
294
    @Override
295
    public void createTestDataSet() throws FileNotFoundException {}
296
}
(2-2/2)