Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 142
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
phpbb_functional_user_password_reset_test
0.00% covered (danger)
0.00%
0 / 142
0.00% covered (danger)
0.00%
0 / 8
132
0.00% covered (danger)
0.00%
0 / 1
 test_password_reset
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 1
2
 test_login_after_reset
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 data_reset_user_password
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 test_reset_user_password
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 test_login
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 test_activateAfterDeactivate
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
2
 test_resendActivation
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
2
 get_user_data
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3*
4* This file is part of the phpBB Forum Software package.
5*
6* @copyright (c) phpBB Limited <https://www.phpbb.com>
7* @license GNU General Public License, version 2 (GPL-2.0)
8*
9* For full copyright and license information, please see
10* the docs/CREDITS.txt file.
11*
12*/
13
14/**
15* @group functional
16*/
17class phpbb_functional_user_password_reset_test extends phpbb_functional_test_case
18{
19    protected $user_data;
20
21    protected const TEST_USER = 'reset-password-test-user';
22
23    protected const TEST_EMAIL = 'reset-password-test-user@test.com';
24
25    public function test_password_reset()
26    {
27        $this->add_lang('ucp');
28        $user_id = $this->create_user(self::TEST_USER, self::TEST_EMAIL);
29
30        // test without email
31        $crawler = self::request('GET', "ucp.php?mode=sendpassword&sid={$this->sid}");
32        $this->assertStringContainsString('app.php/user/forgot_password', $crawler->getUri());
33        $form = $crawler->selectButton('submit')->form();
34        $crawler = self::submit($form);
35        $this->assertContainsLang('NO_EMAIL_USER', $crawler->text());
36
37        // test with non-existent email
38        $crawler = self::request('GET', "app.php/user/forgot_password?sid={$this->sid}");
39        $form = $crawler->selectButton('submit')->form(array(
40            'email'    => 'non-existent@email.com',
41        ));
42        $crawler = self::submit($form);
43        $this->assertContainsLang('PASSWORD_RESET_LINK_SENT', $crawler->text());
44
45        // test with correct email
46        $crawler = self::request('GET', "app.php/user/forgot_password?sid={$this->sid}");
47        $form = $crawler->selectButton('submit')->form(array(
48            'email'        => self::TEST_EMAIL,
49        ));
50        $crawler = self::submit($form);
51        $this->assertContainsLang('PASSWORD_RESET_LINK_SENT', $crawler->text());
52
53        // Check if columns in database were updated for password reset
54        $this->get_user_data(self::TEST_USER);
55        $this->assertNotEmpty($this->user_data['reset_token']);
56        $this->assertNotEmpty($this->user_data['reset_token_expiration']);
57        $reset_token = $this->user_data['reset_token'];
58        $reset_token_expiration = $this->user_data['reset_token_expiration'];
59
60        // Check that reset token is only created once per day
61        $crawler = self::request('GET', "app.php/user/forgot_password?sid={$this->sid}");
62        $form = $crawler->selectButton('submit')->form(array(
63            'email'        => self::TEST_EMAIL,
64        ));
65        $crawler = self::submit($form);
66        $this->assertContainsLang('PASSWORD_RESET_LINK_SENT', $crawler->text());
67
68        $this->get_user_data(self::TEST_USER);
69        $this->assertNotEmpty($this->user_data['reset_token']);
70        $this->assertNotEmpty($this->user_data['reset_token_expiration']);
71        $this->assertEquals($reset_token, $this->user_data['reset_token']);
72        $this->assertEquals($reset_token_expiration, $this->user_data['reset_token_expiration']);
73
74        // Create another user with the same email
75        $this->create_user('reset-password-test-user1', self::TEST_EMAIL);
76
77        // Test that username is now also required
78        $crawler = self::request('GET', "app.php/user/forgot_password?sid={$this->sid}");
79        $form = $crawler->selectButton('submit')->form(array(
80            'email'        => self::TEST_EMAIL,
81        ));
82        $crawler = self::submit($form);
83        $this->assertContainsLang('EMAIL_NOT_UNIQUE', $crawler->text());
84
85        // Provide both username and email
86        $form = $crawler->selectButton('submit')->form(array(
87            'email'        => self::TEST_EMAIL,
88            'username'    => 'reset-password-test-user1',
89        ));
90        $crawler = self::submit($form);
91        $this->assertContainsLang('PASSWORD_RESET_LINK_SENT', $crawler->text());
92
93        // Check if columns in database were updated for password reset
94        $this->get_user_data('reset-password-test-user1');
95        $this->assertNotEmpty($this->user_data['reset_token']);
96        $this->assertNotEmpty($this->user_data['reset_token_expiration']);
97        $this->assertGreaterThan(time(), $this->user_data['reset_token_expiration']);
98    }
99
100    public function test_login_after_reset()
101    {
102        $this->login(self::TEST_USER);
103    }
104
105    public static function data_reset_user_password()
106    {
107        return [
108            ['RESET_TOKEN_EXPIRED_OR_INVALID', 0, 'abcdef'],
109            ['NO_USER', ' ', 'abcdef'],
110            ['NO_RESET_TOKEN', 0, ' '],
111            ['RESET_TOKEN_EXPIRED_OR_INVALID', 2, ''],
112            ['RESET_TOKEN_EXPIRED_OR_INVALID', 1e7, ''],
113            ['', 0, ''],
114            ['NO_RESET_TOKEN', 0, ''], // already reset
115        ];
116    }
117
118    /**
119     * @dataProvider data_reset_user_password
120     */
121    public function test_reset_user_password($expected, $user_id, $token)
122    {
123        $this->add_lang('ucp');
124        $this->get_user_data(self::TEST_USER);
125        $user_id = !$user_id ? $this->user_data['user_id'] : $user_id;
126        $token = !$token ? $this->user_data['reset_token'] : $token;
127
128        $crawler = self::request('GET', "app.php/user/reset_password?u=$user_id&token=" . urlencode($token));
129
130        if ($expected)
131        {
132            $this->assertContainsLang($expected, $crawler->text());
133        }
134        else
135        {
136            $form = $crawler->filter('input[type=submit]')->form();
137            $values = array_merge($form->getValues(), [
138                'new_password'            => self::TEST_USER,
139                'new_password_confirm'    => self::TEST_USER,
140            ]);
141            $crawler = self::submit($form, $values);
142            $this->assertContainsLang('PASSWORD_RESET', $crawler->text());
143        }
144    }
145
146    public function test_login()
147    {
148        $this->add_lang('ucp');
149        $crawler = self::request('GET', 'ucp.php');
150        $this->assertStringContainsString($this->lang('LOGIN_EXPLAIN_UCP'), $crawler->filter('html')->text());
151
152        $form = $crawler->selectButton($this->lang('LOGIN'))->form();
153        $crawler = self::submit($form, array('username' => self::TEST_USER, 'password' => self::TEST_USER));
154        $this->assertStringNotContainsString($this->lang('LOGIN'), $crawler->filter('.navbar')->text());
155
156        $this->logout();
157
158        $crawler = self::request('GET', 'ucp.php');
159        $this->assertStringContainsString($this->lang('LOGIN_EXPLAIN_UCP'), $crawler->filter('html')->text());
160
161        $form = $crawler->selectButton($this->lang('LOGIN'))->form();
162        // Try logging in with the old password
163        $crawler = self::submit($form, array('username' => self::TEST_USER, 'password' => 'reset-password-test-userreset-password-test-user'));
164        $this->assertStringContainsString($this->lang('LOGIN_ERROR_PASSWORD', '', ''), $crawler->filter('html')->text());
165    }
166
167    /**
168     * @depends test_login
169     */
170    public function test_activateAfterDeactivate()
171    {
172        // User is active, actkey should not exist
173        $this->get_user_data(self::TEST_USER);
174        $this->assertEmpty($this->user_data['user_actkey']);
175
176        $this->login();
177        $this->admin_login();
178        $this->add_lang('acp/users');
179
180        // Go to user account page
181        $crawler = self::request('GET', 'adm/index.php?i=acp_users&mode=overview&sid=' . $this->sid);
182        $this->assertContainsLang('FIND_USERNAME', $crawler->filter('html')->text());
183
184        $form = $crawler->selectButton('Submit')->form();
185        $crawler = self::submit($form, array('username' => self::TEST_USER));
186
187        // Deactivate account and go back to overview of current user
188        $this->assertContainsLang('USER_TOOLS', $crawler->filter('html')->text());
189        $form = $crawler->filter('input[name=update]')->selectButton('Submit')->form();
190        $crawler = self::submit($form, array('action' => 'active'));
191
192        $this->assertContainsLang('USER_ADMIN_DEACTIVED', $crawler->filter('html')->text());
193        $link = $crawler->selectLink('Back to previous page')->link();
194        $crawler = self::request('GET', preg_replace('#(.+)(adm/index.php.+)#', '$2', $link->getUri()));
195
196        // Ensure again that actkey is empty after deactivation
197        $this->get_user_data(self::TEST_USER);
198        $this->assertEmpty($this->user_data['user_actkey']);
199
200        // Force reactivation of account and check that act key is not empty anymore
201        $this->assertContainsLang('USER_TOOLS', $crawler->filter('html')->text());
202        $form = $crawler->filter('input[name=update]')->selectButton('Submit')->form();
203        $crawler = self::submit($form, array('action' => 'reactivate'));
204        $this->assertContainsLang('FORCE_REACTIVATION_SUCCESS', $crawler->filter('html')->text());
205
206        $this->get_user_data(self::TEST_USER);
207        $this->assertNotEmpty($this->user_data['user_actkey']);
208
209        // Logout and try resending activation email, account is deactivated though
210        $this->logout();
211        $this->add_lang('ucp');
212
213        $crawler = self::request('GET', 'ucp.php?mode=resend_act');
214        $this->assertContainsLang('UCP_RESEND', $crawler->filter('html')->text());
215        $form = $crawler->filter('input[name=submit]')->selectButton('Submit')->form();
216        $crawler = self::submit($form, [
217            'username'        => self::TEST_USER,
218            'email'            => self::TEST_EMAIL,
219        ]);
220        $this->assertContainsLang('ACCOUNT_DEACTIVATED', $crawler->filter('html')->text());
221    }
222
223    /**
224     * @depends test_activateAfterDeactivate
225     */
226    public function test_resendActivation()
227    {
228        // User is deactivated and should have actkey, actkey should not exist
229        $this->get_user_data(self::TEST_USER);
230        $this->assertNotEmpty($this->user_data['user_actkey']);
231
232        // Change reason for inactivity
233        $sql = 'UPDATE ' . USERS_TABLE . '
234            SET user_inactive_reason = ' . INACTIVE_REMIND . '
235            WHERE user_id = ' . (int) $this->user_data['user_id'];
236        $this->db->sql_query($sql);
237
238        $this->add_lang('ucp');
239
240        $crawler = self::request('GET', 'ucp.php?mode=resend_act');
241        $this->assertContainsLang('UCP_RESEND', $crawler->filter('html')->text());
242        $form = $crawler->filter('input[name=submit]')->selectButton('Submit')->form();
243        $crawler = self::submit($form, [
244            'username'        => self::TEST_USER,
245            'email'            => self::TEST_EMAIL,
246        ]);
247        $this->assertContainsLang('ACTIVATION_ALREADY_SENT', $crawler->filter('html')->text());
248    }
249
250    protected function get_user_data($username)
251    {
252        $sql = 'SELECT user_id, username, user_type, user_email, user_newpasswd, user_lang, user_actkey, user_inactive_reason, reset_token, reset_token_expiration
253            FROM ' . USERS_TABLE . "
254            WHERE username = '" . $this->db->sql_escape($username) . "'";
255        $result = $this->db->sql_query($sql);
256        $this->user_data = $this->db->sql_fetchrow($result);
257        $this->db->sql_freeresult($result);
258    }
259}