Skip to content

Commit d3283de

Browse files
committed
Basic Unit tests for the mig/shared/auth.py module.
1 parent a37ea11 commit d3283de

File tree

1 file changed

+189
-0
lines changed

1 file changed

+189
-0
lines changed

tests/test_mig_shared_auth.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# --- BEGIN_HEADER ---
4+
#
5+
# test_mig_shared_auth - unit tests for authentication helpers
6+
# Copyright (C) 2003-2025 The MiG Project by the Science HPC Center at UCPH
7+
#
8+
# This file is part of MiG.
9+
#
10+
# MiG is free software: you can redistribute it and/or modify
11+
# it under the terms of the GNU General Public License as published by
12+
# the Free Software Foundation; either version 2 of the License, or
13+
# (at your option) any later version.
14+
#
15+
# MiG is distributed in the hope that it will be useful,
16+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
# GNU General Public License for more details.
19+
#
20+
# You should have received a copy of the GNU General Public License
21+
# along with this program; if not, write to the Free Software
22+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
23+
# USA.
24+
#
25+
# --- END_HEADER ---
26+
#
27+
28+
"""Unit tests for authentication functionality in mig/shared/auth.py"""
29+
30+
import datetime
31+
import http.cookies
32+
import os
33+
import pickle
34+
import time
35+
import unittest
36+
37+
from tests.support import MigTestCase, testmain, ensure_dirs_exist
38+
39+
import mig.shared.auth as auth
40+
from mig.shared.base import client_id_dir
41+
from mig.shared.defaults import twofactor_key_bytes, twofactor_cookie_ttl
42+
43+
TEST_USER_DN = \
44+
'/C=DK/ST=NA/L=NA/O=Test Org/OU=NA/CN=Test User/emailAddress=test@example.com'
45+
46+
47+
class MigSharedAuth__twofactor(MigTestCase):
48+
"""Unit tests for two-factor authentication in auth module"""
49+
50+
def _provide_configuration(self):
51+
return 'testconfig'
52+
53+
def before_each(self):
54+
"""Setup test environment before each test method"""
55+
ensure_dirs_exist(self.configuration.user_cache)
56+
ensure_dirs_exist(self.configuration.user_pending)
57+
ensure_dirs_exist(self.configuration.user_settings)
58+
ensure_dirs_exist(self.configuration.mrsl_files_dir)
59+
ensure_dirs_exist(self.configuration.resource_pending)
60+
ensure_dirs_exist(self.configuration.twofactor_home)
61+
62+
def test_get_twofactor_secrets_generates_valid_key(self):
63+
"""Test get_twofactor_secrets generates and stores valid key"""
64+
self._provision_test_user(self, TEST_USER_DN)
65+
66+
# Exercise functionality
67+
b32_key, interval, otp_uri = auth.get_twofactor_secrets(
68+
self.configuration, TEST_USER_DN)
69+
70+
# Verify results
71+
self.assertIsNotNone(b32_key)
72+
self.assertEqual(
73+
len(b32_key), twofactor_key_bytes,
74+
'generated key length should match configured bytes'
75+
)
76+
self.assertIn(
77+
'otpauth://', otp_uri,
78+
'OTP URI should use standard provisioning format'
79+
)
80+
self.assertIn(
81+
self.configuration.short_title, otp_uri,
82+
'OTP URI should include site title as issuer'
83+
)
84+
85+
def test_verify_twofactor_token_valid(self):
86+
"""Test valid token verification"""
87+
self._provision_test_user(self, TEST_USER_DN)
88+
b32_key, _, _ = auth.get_twofactor_secrets(
89+
self.configuration, TEST_USER_DN)
90+
91+
# Generate current valid token
92+
totp = auth.get_totp(TEST_USER_DN, b32_key, self.configuration)
93+
valid_token = totp.now()
94+
95+
# Verify token
96+
result = auth.verify_twofactor_token(self.configuration, TEST_USER_DN,
97+
b32_key, valid_token)
98+
99+
self.assertTrue(result, 'valid token should be accepted')
100+
101+
def test_verify_twofactor_token_invalid(self):
102+
"""Test invalid token rejection"""
103+
self._provision_test_user(self, TEST_USER_DN)
104+
b32_key, _, _ = auth.get_twofactor_secrets(
105+
self.configuration, TEST_USER_DN)
106+
invalid_token = '000000'
107+
108+
result = auth.verify_twofactor_token(self.configuration, TEST_USER_DN,
109+
b32_key, invalid_token)
110+
111+
self.assertFalse(result, 'invalid token should be rejected')
112+
113+
def test_reset_twofactor_key(self):
114+
"""Test twofactor key reset changes stored key"""
115+
self._provision_test_user(self, TEST_USER_DN)
116+
original_key, _, _ = auth.get_twofactor_secrets(self.configuration,
117+
TEST_USER_DN)
118+
119+
# Reset key
120+
new_key_b32 = auth.reset_twofactor_key(TEST_USER_DN,
121+
self.configuration)
122+
new_key = new_key_b32.decode('utf8')
123+
124+
# Verify change
125+
self.assertNotEqual(original_key, new_key, 'new key should differ')
126+
self.assertEqual(len(new_key), twofactor_key_bytes,
127+
'new key should have correct length')
128+
129+
# Verify persistence
130+
reloaded_key = auth.load_twofactor_key(TEST_USER_DN,
131+
self.configuration)
132+
#
133+
self.assertEqual(new_key, reloaded_key, 'key should persist')
134+
135+
def test_twofactor_session_lifecycle(self):
136+
"""Test full twofactor session lifecycle"""
137+
client_dir = self._provision_test_user(self, TEST_USER_DN)
138+
user_addr = '127.0.0.1'
139+
user_agent = 'TestAgent'
140+
session_start = time.time()
141+
session_cookie = ''
142+
143+
# Generate session
144+
session_key = auth.generate_session_key(self.configuration,
145+
TEST_USER_DN)
146+
save_result = auth.save_twofactor_session(self.configuration,
147+
TEST_USER_DN, session_key,
148+
user_addr, user_agent,
149+
session_start)
150+
self.assertTrue(save_result, 'session should save successfully')
151+
152+
# Verify session exists
153+
sessions = auth.list_twofactor_sessions(self.configuration,
154+
TEST_USER_DN)
155+
self.assertIn(session_key, sessions, 'new session should be listed')
156+
157+
# Validate session details
158+
session_data = auth.load_twofactor_session(self.configuration,
159+
session_key)
160+
self.assertEqual(session_data['client_id'], TEST_USER_DN,
161+
'session should match client_id'
162+
)
163+
self.assertEqual(session_data['session_end'], session_start +
164+
twofactor_cookie_ttl,
165+
'session should have correct TTL')
166+
167+
# Mimic cookie init
168+
cookie = http.cookies.SimpleCookie()
169+
session_start = time.time()
170+
cookie['2FA_Auth'] = session_key
171+
cookie['2FA_Auth']['path'] = '/'
172+
# NOTE: SimpleCookie translates expires ttl to actual date from now
173+
cookie['2FA_Auth']['expires'] = twofactor_cookie_ttl
174+
cookie['2FA_Auth']['secure'] = True
175+
cookie['2FA_Auth']['httponly'] = True
176+
177+
environ = {'HTTP_COOKIE': cookie}
178+
# Expire session
179+
expire_result = auth.expire_twofactor_session(self.configuration,
180+
TEST_USER_DN, environ)
181+
self.assertTrue(expire_result, 'session should expire successfully')
182+
sessions_after = auth.list_twofactor_sessions(self.configuration,
183+
TEST_USER_DN)
184+
self.assertNotIn(session_key, sessions_after,
185+
'session should be removed')
186+
187+
188+
if __name__ == '__main__':
189+
testmain()

0 commit comments

Comments
 (0)