• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

fiduswriter / fiduswriter / 24113311951

08 Apr 2026 01:47AM UTC coverage: 86.61% (-0.8%) from 87.363%
24113311951

push

github

web-flow
Merge pull request #1375 from fiduswriter/feature/anonymous-editing

Feature/open editing access, #1359

7775 of 8977 relevant lines covered (86.61%)

5.16 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.66
fiduswriter/user/tests/test_two_factor.py
1
"""
2
Selenium tests for two-factor authentication functionality.
3
"""
4

5
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
1✔
6
from django.contrib.auth import get_user_model
1✔
7

8
from testing.selenium_helper import SeleniumHelper
1✔
9
from selenium.webdriver.support.wait import WebDriverWait
1✔
10
from selenium.webdriver.support import expected_conditions as EC
1✔
11
from selenium.webdriver.common.by import By
1✔
12
import pyotp
1✔
13
import time
1✔
14
import datetime
1✔
15

16

17
class TwoFactorTests(SeleniumHelper, StaticLiveServerTestCase):
1✔
18
    """Test suite for two-factor authentication features."""
19

20
    def setUp(self):
1✔
21
        super().setUp()
1✔
22
        # Create test users
23
        self.user1 = self.create_user(
1✔
24
            username="testuser1",
25
            email="test1@example.com",
26
            passtext="testpass123",
27
        )
28
        self.user2 = self.create_user(
1✔
29
            username="testuser2",
30
            email="test2@example.com",
31
            passtext="testpass456",
32
        )
33

34
        # Setup drivers
35
        drivers_info = self.get_drivers(1)
1✔
36
        self.clients = drivers_info["clients"]
1✔
37
        self.drivers = drivers_info["drivers"]
1✔
38
        self.wait_time = drivers_info["wait_time"]
1✔
39

40
        # Login user for profile access tests
41
        self.login_user(self.user1, self.drivers[0], self.clients[0])
1✔
42

43
        # Navigate to profile page
44
        self.drivers[0].get(f"{self.live_server_url}/user/profile/")
1✔
45

46
        # Wait for profile page to load
47
        WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
48
            EC.presence_of_element_located((By.ID, "profile-wrapper"))
49
        )
50

51
    def test_two_factor_status_check(self):
1✔
52
        """Test checking two-factor authentication status."""
53
        # Initial status should be disabled
54
        response = self.clients[0].post(
1✔
55
            "/api/user/two-factor/status/",
56
            HTTP_X_REQUESTED_WITH="XMLHttpRequest",
57
        )
58
        self.assertEqual(response.status_code, 200)
1✔
59
        data = response.json()
1✔
60
        self.assertEqual(data["status"], "success")
1✔
61
        self.assertFalse(data["enabled"])
1✔
62

63
    def test_two_factor_setup_flow(self):
1✔
64
        """Test the complete flow of setting up two-factor authentication."""
65
        # Click on setup two-factor button
66
        setup_btn = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
67
            EC.element_to_be_clickable((By.ID, "setup-two-factor"))
68
        )
69
        setup_btn.click()
1✔
70

71
        # Wait for the dialog to appear
72
        time.sleep(1)  # Allow dialog animation
1✔
73

74
        # Check that dialog elements are present
75
        dialog = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
76
            EC.presence_of_element_located((By.ID, "two-factor-setup-dialog"))
77
        )
78
        self.assertIsNotNone(dialog)
1✔
79

80
        # Check QR code container
81
        qr_container = self.drivers[0].find_element(
1✔
82
            By.CLASS_NAME, "two-factor-qr-container"
83
        )
84
        self.assertIsNotNone(qr_container)
1✔
85

86
        # Check secret key is displayed
87
        secret_code = self.drivers[0].find_element(
1✔
88
            By.CLASS_NAME, "two-factor-secret"
89
        )
90
        self.assertIsNotNone(secret_code)
1✔
91
        secret_key = secret_code.text.strip()
1✔
92

93
        # Verify secret key format (base32 - uppercase letters and numbers 2-7)
94
        # Standard base32 uses A-Z and 2-7, typically 32 characters with padding
95
        self.assertGreater(len(secret_key), 0)
1✔
96
        self.assertTrue(
1✔
97
            all(c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=" for c in secret_key),
98
            f"Secret key contains invalid base32 characters: {secret_key}",
99
        )
100

101
        # Generate a valid TOTP code using the secret
102
        totp = pyotp.TOTP(secret_key)
1✔
103
        valid_code = totp.now()
1✔
104

105
        # Enter the code
106
        code_input = self.drivers[0].find_element(By.ID, "two-factor-code")
1✔
107
        code_input.clear()
1✔
108
        code_input.send_keys(valid_code)
1✔
109

110
        # Click verify button
111
        verify_btn = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
112
            EC.element_to_be_clickable(
113
                (
114
                    By.XPATH,
115
                    "//button[contains(@class, 'fw-dark') and contains(text(), 'Verify')]",
116
                )
117
            )
118
        )
119
        verify_btn.click()
1✔
120

121
        # Wait for success
122
        time.sleep(2)  # Allow API call to complete
1✔
123

124
        # Check that 2FA is now enabled in UI
125
        enabled_status = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
126
            EC.presence_of_element_located(
127
                (By.ID, "two-factor-enabled-status")
128
            )
129
        )
130
        self.assertIsNotNone(enabled_status)
1✔
131

132
        # Verify via API
133
        response = self.clients[0].post(
1✔
134
            "/api/user/two-factor/status/",
135
            HTTP_X_REQUESTED_WITH="XMLHttpRequest",
136
        )
137
        data = response.json()
1✔
138
        self.assertTrue(data["enabled"])
1✔
139

140
    def test_two_factor_invalid_code(self):
1✔
141
        """Test that invalid codes are rejected during setup."""
142
        # Start setup process
143
        setup_btn = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
144
            EC.element_to_be_clickable((By.ID, "setup-two-factor"))
145
        )
146
        setup_btn.click()
1✔
147

148
        time.sleep(1)
1✔
149

150
        # Enter invalid code
151
        code_input = self.drivers[0].find_element(By.ID, "two-factor-code")
1✔
152
        code_input.clear()
1✔
153
        code_input.send_keys("000000")  # Invalid code
1✔
154

155
        # Click verify button
156
        verify_btn = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
157
            EC.element_to_be_clickable(
158
                (
159
                    By.XPATH,
160
                    "//button[contains(@class, 'fw-dark') and contains(text(), 'Verify')]",
161
                )
162
            )
163
        )
164
        verify_btn.click()
1✔
165

166
        time.sleep(1)
1✔
167

168
        # Verify that dialog is still open (error occurred)
169
        dialog = self.drivers[0].find_element(By.ID, "two-factor-setup-dialog")
1✔
170
        self.assertIsNotNone(dialog)
1✔
171

172
        # Check that 2FA is still disabled
173
        response = self.clients[0].post(
1✔
174
            "/api/user/two-factor/status/",
175
            HTTP_X_REQUESTED_WITH="XMLHttpRequest",
176
        )
177
        data = response.json()
1✔
178
        self.assertFalse(data["enabled"])
1✔
179

180
    def test_two_factor_disable(self):
1✔
181
        """Test disabling two-factor authentication."""
182
        # First, enable 2FA
183
        setup_btn = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
184
            EC.element_to_be_clickable((By.ID, "setup-two-factor"))
185
        )
186
        setup_btn.click()
1✔
187

188
        time.sleep(1)
1✔
189

190
        secret_code = (
1✔
191
            self.drivers[0]
192
            .find_element(By.CLASS_NAME, "two-factor-secret")
193
            .text.strip()
194
        )
195
        totp = pyotp.TOTP(secret_code)
1✔
196
        valid_code = totp.now()
1✔
197

198
        code_input = self.drivers[0].find_element(By.ID, "two-factor-code")
1✔
199
        code_input.clear()
1✔
200
        code_input.send_keys(valid_code)
1✔
201

202
        # The verify button is in the dialog buttons, not a separate ID
203
        # Find it by text content in the dialog
204
        verify_btn = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
205
            EC.element_to_be_clickable(
206
                (
207
                    By.XPATH,
208
                    "//button[contains(@class, 'fw-dark') and contains(text(), 'Verify')]",
209
                )
210
            )
211
        )
212
        verify_btn.click()
1✔
213

214
        time.sleep(2)
1✔
215

216
        # Verify 2FA is enabled
217
        response = self.clients[0].post(
1✔
218
            "/api/user/two-factor/status/",
219
            HTTP_X_REQUESTED_WITH="XMLHttpRequest",
220
        )
221
        data = response.json()
1✔
222
        self.assertTrue(data["enabled"])
1✔
223

224
        # Now disable it
225
        disable_btn = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
226
            EC.element_to_be_clickable((By.ID, "disable-two-factor"))
227
        )
228
        disable_btn.click()
1✔
229

230
        time.sleep(1)
1✔
231

232
        # Click confirm disable button in dialog (also by text content)
233
        disable_confirm_btn = WebDriverWait(
1✔
234
            self.drivers[0], self.wait_time
235
        ).until(
236
            EC.element_to_be_clickable(
237
                (
238
                    By.XPATH,
239
                    "//button[contains(@class, 'fw-orange') and contains(text(), 'Disable')]",
240
                )
241
            )
242
        )
243
        disable_confirm_btn.click()
1✔
244

245
        time.sleep(2)
1✔
246

247
        # Verify 2FA is disabled
248
        response = self.clients[0].post(
1✔
249
            "/api/user/two-factor/status/",
250
            HTTP_X_REQUESTED_WITH="XMLHttpRequest",
251
        )
252
        data = response.json()
1✔
253
        self.assertFalse(data["enabled"])
1✔
254

255
    def test_two_factor_login_flow(self):
1✔
256
        """Test login with two-factor authentication enabled."""
257
        # Enable 2FA for user2
258
        self.login_user(self.user2, self.drivers[0], self.clients[0])
1✔
259
        self.drivers[0].get(f"{self.live_server_url}/user/profile/")
1✔
260

261
        setup_btn = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
262
            EC.element_to_be_clickable((By.ID, "setup-two-factor"))
263
        )
264
        setup_btn.click()
1✔
265

266
        time.sleep(1)
1✔
267

268
        secret_code = (
1✔
269
            self.drivers[0]
270
            .find_element(By.CLASS_NAME, "two-factor-secret")
271
            .text.strip()
272
        )
273
        totp = pyotp.TOTP(secret_code)
1✔
274
        valid_code = totp.now()
1✔
275

276
        code_input = self.drivers[0].find_element(By.ID, "two-factor-code")
1✔
277
        code_input.clear()
1✔
278
        code_input.send_keys(valid_code)
1✔
279

280
        verify_btn = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
281
            EC.element_to_be_clickable(
282
                (
283
                    By.XPATH,
284
                    "//button[contains(@class, 'fw-dark') and contains(text(), 'Verify')]",
285
                )
286
            )
287
        )
288
        verify_btn.click()
1✔
289

290
        time.sleep(2)
1✔
291

292
        # Wait until a new TOTP 30-second window begins. The setup
293
        # verification above called device.verify_token() which records
294
        # the current timestep in last_t. If we attempt login in the
295
        # same window, django_otp rejects the code as a replay.
296
        current_time = datetime.datetime.now().timestamp()
1✔
297
        seconds_into_window = int(current_time) % 30
1✔
298
        time.sleep(30 - seconds_into_window + 1)
1✔
299

300
        # Logout
301
        self.logout_user(self.drivers[0], self.clients[0])
1✔
302

303
        # Try to login
304
        self.drivers[0].get(f"{self.live_server_url}/")
1✔
305

306
        # Enter username and password
307
        login_input = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
308
            EC.presence_of_element_located((By.ID, "id-login"))
309
        )
310
        login_input.send_keys("testuser2")
1✔
311

312
        password_input = self.drivers[0].find_element(By.ID, "id-password")
1✔
313
        password_input.send_keys("testpass456")
1✔
314

315
        submit_btn = self.drivers[0].find_element(By.ID, "login-submit")
1✔
316
        submit_btn.click()
1✔
317

318
        # Wait for 2FA dialog to appear
319
        time.sleep(2)
1✔
320

321
        # Check that 2FA dialog appeared
322
        two_fa_dialog = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
323
            EC.presence_of_element_located((By.ID, "two-factor-login-dialog"))
324
        )
325
        self.assertIsNotNone(two_fa_dialog)
1✔
326

327
        # Generate a FRESH TOTP code with better timing protection for CI
328
        # Always wait for a fresh window to avoid timing issues
329
        current_time = datetime.datetime.now().timestamp()
1✔
330
        time_in_window = int(current_time) % 30
1✔
331

332
        # If we're in the last 10 seconds OR first 2 seconds of a window, wait for a fresh window
333
        # This gives us a stable 18-second window to use the code
334
        if time_in_window > 20 or time_in_window < 2:
1✔
335
            wait_time = (
×
336
                32 - time_in_window
337
                if time_in_window > 20
338
                else 2 - time_in_window + 1
339
            )
340
            time.sleep(wait_time)
×
341

342
        # Generate code and verify it's valid locally before using it
343
        fresh_code = totp.now()
1✔
344
        # Double-check the code is valid with a larger tolerance window for CI
345
        self.assertTrue(
1✔
346
            totp.verify(fresh_code, valid_window=2),
347
            f"Generated code {fresh_code} is not valid locally",
348
        )
349

350
        code_input = self.drivers[0].find_element(By.ID, "two-factor-code")
1✔
351
        code_input.clear()
1✔
352
        code_input.send_keys(fresh_code)
1✔
353

354
        # Verify
355
        verify_btn = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
356
            EC.element_to_be_clickable(
357
                (
358
                    By.XPATH,
359
                    "//button[contains(@class, 'fw-dark') and contains(text(), 'Verify')]",
360
                )
361
            )
362
        )
363
        verify_btn.click()
1✔
364

365
        # Wait for 2FA dialog to close (successful login)
366
        WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
367
            EC.invisibility_of_element_located(
368
                (By.ID, "two-factor-login-dialog")
369
            )
370
        )
371

372
        # Wait a bit for any redirects
373
        time.sleep(2)
1✔
374

375
        # Verify we're logged in by checking we can access a protected page
376
        self.drivers[0].get(f"{self.live_server_url}/user/profile/")
1✔
377

378
        # If we can see the profile page, login was successful
379
        WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
380
            EC.presence_of_element_located((By.ID, "profile-wrapper"))
381
        )
382

383
        # Also verify that the 2FA status shows as enabled on the profile page
384
        two_fa_status = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
385
            EC.visibility_of_element_located(
386
                (By.ID, "two-factor-enabled-status")
387
            )
388
        )
389
        self.assertIsNotNone(two_fa_status)
1✔
390

391
    def test_two_factor_login_invalid_code(self):
1✔
392
        """Test login with invalid 2FA code."""
393
        # Enable 2FA for user2
394
        self.login_user(self.user2, self.drivers[0], self.clients[0])
1✔
395
        self.drivers[0].get(f"{self.live_server_url}/user/profile/")
1✔
396

397
        setup_btn = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
398
            EC.element_to_be_clickable((By.ID, "setup-two-factor"))
399
        )
400
        setup_btn.click()
1✔
401

402
        time.sleep(1)
1✔
403

404
        secret_code = (
1✔
405
            self.drivers[0]
406
            .find_element(By.CLASS_NAME, "two-factor-secret")
407
            .text.strip()
408
        )
409
        totp = pyotp.TOTP(secret_code)
1✔
410
        valid_code = totp.now()
1✔
411

412
        code_input = self.drivers[0].find_element(By.ID, "two-factor-code")
1✔
413
        code_input.clear()
1✔
414
        code_input.send_keys(valid_code)
1✔
415

416
        verify_btn = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
417
            EC.element_to_be_clickable(
418
                (
419
                    By.XPATH,
420
                    "//button[contains(@class, 'fw-dark') and contains(text(), 'Verify')]",
421
                )
422
            )
423
        )
424
        verify_btn.click()
1✔
425

426
        time.sleep(2)
1✔
427

428
        # Logout
429
        self.logout_user(self.drivers[0], self.clients[0])
1✔
430

431
        # Try to login
432
        self.drivers[0].get(f"{self.live_server_url}/")
1✔
433

434
        login_input = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
435
            EC.presence_of_element_located((By.ID, "id-login"))
436
        )
437
        login_input.send_keys("testuser2")
1✔
438

439
        password_input = self.drivers[0].find_element(By.ID, "id-password")
1✔
440
        password_input.send_keys("testpass456")
1✔
441

442
        submit_btn = self.drivers[0].find_element(By.ID, "login-submit")
1✔
443
        submit_btn.click()
1✔
444

445
        # Wait for 2FA dialog
446
        WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
447
            EC.presence_of_element_located((By.ID, "two-factor-login-dialog"))
448
        )
449

450
        # Enter invalid code
451
        code_input = self.drivers[0].find_element(By.ID, "two-factor-code")
1✔
452
        code_input.clear()
1✔
453
        code_input.send_keys("000000")  # Invalid code
1✔
454

455
        verify_btn = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
456
            EC.element_to_be_clickable(
457
                (
458
                    By.XPATH,
459
                    "//button[contains(@class, 'fw-dark') and contains(text(), 'Verify')]",
460
                )
461
            )
462
        )
463
        verify_btn.click()
1✔
464

465
        time.sleep(1)
1✔
466

467
        # Dialog should still be open
468
        two_fa_dialog = self.drivers[0].find_element(
1✔
469
            By.ID, "two-factor-login-dialog"
470
        )
471
        self.assertIsNotNone(two_fa_dialog)
1✔
472

473
    def test_two_factor_multiple_failed_attempts(self):
1✔
474
        """Test that multiple failed 2FA attempts lock out the user."""
475
        # Enable 2FA
476
        self.login_user(self.user2, self.drivers[0], self.clients[0])
1✔
477
        self.drivers[0].get(f"{self.live_server_url}/user/profile/")
1✔
478

479
        setup_btn = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
480
            EC.element_to_be_clickable((By.ID, "setup-two-factor"))
481
        )
482
        setup_btn.click()
1✔
483

484
        time.sleep(1)
1✔
485

486
        secret_code = (
1✔
487
            self.drivers[0]
488
            .find_element(By.CLASS_NAME, "two-factor-secret")
489
            .text.strip()
490
        )
491
        totp = pyotp.TOTP(secret_code)
1✔
492
        valid_code = totp.now()
1✔
493

494
        code_input = self.drivers[0].find_element(By.ID, "two-factor-code")
1✔
495
        code_input.clear()
1✔
496
        code_input.send_keys(valid_code)
1✔
497

498
        verify_btn = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
499
            EC.element_to_be_clickable(
500
                (
501
                    By.XPATH,
502
                    "//button[contains(@class, 'fw-dark') and contains(text(), 'Verify')]",
503
                )
504
            )
505
        )
506
        verify_btn.click()
1✔
507

508
        time.sleep(2)
1✔
509

510
        # Logout
511
        self.logout_user(self.drivers[0], self.clients[0])
1✔
512

513
        # Try to login with failed 2FA attempts
514
        self.drivers[0].get(f"{self.live_server_url}/")
1✔
515

516
        login_input = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
517
            EC.presence_of_element_located((By.ID, "id-login"))
518
        )
519
        login_input.send_keys("testuser2")
1✔
520

521
        password_input = self.drivers[0].find_element(By.ID, "id-password")
1✔
522
        password_input.send_keys("testpass456")
1✔
523

524
        submit_btn = self.drivers[0].find_element(By.ID, "login-submit")
1✔
525
        submit_btn.click()
1✔
526

527
        # Wait for 2FA dialog
528
        WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
529
            EC.presence_of_element_located((By.ID, "two-factor-login-dialog"))
530
        )
531

532
        # Make 3 failed attempts
533
        for i in range(3):
1✔
534
            code_input = self.drivers[0].find_element(By.ID, "two-factor-code")
1✔
535
            code_input.clear()
1✔
536
            code_input.send_keys("000000")
1✔
537

538
            verify_btn = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
539
                EC.element_to_be_clickable(
540
                    (
541
                        By.XPATH,
542
                        "//button[contains(@class, 'fw-dark') and contains(text(), 'Verify')]",
543
                    )
544
                )
545
            )
546
            verify_btn.click()
1✔
547

548
            time.sleep(1)
1✔
549

550
            # Note: The warning check might need adjustment based on actual implementation
551
            # This test may need to be updated if warnings are shown differently
552

553
    def test_two_factor_code_format_validation(self):
1✔
554
        """Test that code format is validated."""
555
        setup_btn = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
556
            EC.element_to_be_clickable((By.ID, "setup-two-factor"))
557
        )
558
        setup_btn.click()
1✔
559

560
        time.sleep(1)
1✔
561

562
        # Try with too short code
563
        code_input = self.drivers[0].find_element(By.ID, "two-factor-code")
1✔
564
        code_input.clear()
1✔
565
        code_input.send_keys("123")
1✔
566

567
        verify_btn = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
568
            EC.element_to_be_clickable(
569
                (
570
                    By.XPATH,
571
                    "//button[contains(@class, 'fw-dark') and contains(text(), 'Verify')]",
572
                )
573
            )
574
        )
575
        verify_btn.click()
1✔
576

577
        # Wait for error alert to appear
578
        time.sleep(1)
1✔
579

580
        # Check that error alert appeared (validation failed)
581
        alert = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
582
            EC.presence_of_element_located((By.CSS_SELECTOR, ".alerts-error"))
583
        )
584
        self.assertIsNotNone(alert)
1✔
585

586
        # Dialog should still be open
587
        dialog = self.drivers[0].find_element(By.ID, "two-factor-setup-dialog")
1✔
588
        self.assertIsNotNone(dialog)
1✔
589

590
    def test_two_factor_qr_code_generation(self):
1✔
591
        """Test that QR code is properly generated."""
592
        setup_btn = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
593
            EC.element_to_be_clickable((By.ID, "setup-two-factor"))
594
        )
595
        setup_btn.click()
1✔
596

597
        time.sleep(1)
1✔
598

599
        # Check QR code canvas element (generated by qrcode library)
600
        qr_canvas = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
601
            EC.presence_of_element_located(
602
                (By.CSS_SELECTOR, ".two-factor-qr-container canvas")
603
            )
604
        )
605
        self.assertIsNotNone(qr_canvas)
1✔
606

607
        # Verify canvas has content (width and height should be set)
608
        width = qr_canvas.get_attribute("width")
1✔
609
        height = qr_canvas.get_attribute("height")
1✔
610
        self.assertGreater(int(width), 0)
1✔
611
        self.assertGreater(int(height), 0)
1✔
612

613
    def test_two_factor_totp_codes_change_over_time(self):
1✔
614
        """Test that TOTP codes change over time."""
615
        setup_btn = WebDriverWait(self.drivers[0], self.wait_time).until(
1✔
616
            EC.element_to_be_clickable((By.ID, "setup-two-factor"))
617
        )
618
        setup_btn.click()
1✔
619

620
        time.sleep(1)
1✔
621

622
        secret_code = (
1✔
623
            self.drivers[0]
624
            .find_element(By.CLASS_NAME, "two-factor-secret")
625
            .text.strip()
626
        )
627
        totp = pyotp.TOTP(secret_code)
1✔
628

629
        code1 = totp.now()
1✔
630
        time.sleep(31)  # Wait for next time window
1✔
631
        code2 = totp.now()
1✔
632

633
        # Codes should be different
634
        self.assertNotEqual(code1, code2)
1✔
635

636
        # The second code should be valid now
637
        self.assertTrue(totp.verify(code2))
1✔
638

639
        # Both should be 6 digits
640
        self.assertEqual(len(code1), 6)
1✔
641
        self.assertEqual(len(code2), 6)
1✔
642

643
    def tearDown(self):
1✔
644
        """Clean up after tests."""
645
        try:
1✔
646
            # Logout users
647
            for driver in self.drivers:
1✔
648
                try:
1✔
649
                    driver.delete_cookie("sessionid")
1✔
650
                    self.leave_site(driver)
1✔
651
                except Exception:
×
652
                    pass  # Ignore errors during cleanup
×
653

654
            # Clean up test users
655
            User = get_user_model()
1✔
656
            User.objects.filter(
1✔
657
                username__in=["testuser1", "testuser2"]
658
            ).delete()
659
        finally:
660
            # Always call parent tearDown
661
            super().tearDown()
1✔
662
            # Explicitly quit drivers to ensure browser windows close
663
            for driver in self.drivers:
1✔
664
                try:
1✔
665
                    driver.quit()
1✔
666
                except Exception:
×
667
                    pass
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc