2727
2828"""Unit tests for PanDA authentication utilities."""
2929
30+ import base64
31+ import json
3032import os
3133import unittest
34+ from datetime import UTC , datetime , timedelta
3235from unittest import mock
3336
3437from lsst .ctrl .bps .panda import __version__ as version
35- from lsst .ctrl .bps .panda .panda_auth_utils import panda_auth_status
38+ from lsst .ctrl .bps .panda .panda_auth_utils import (
39+ TokenExpiredError ,
40+ panda_auth_refresh ,
41+ panda_auth_status ,
42+ )
43+
44+
45+ def make_fake_jwt (exp_offset_days ):
46+ """Return a fake id_token that expires in N days."""
47+ payload = {"exp" : int ((datetime .now (UTC ) + timedelta (days = exp_offset_days )).timestamp ())}
48+ b64_payload = base64 .urlsafe_b64encode (json .dumps (payload ).encode ()).decode ().rstrip ("=" )
49+ return f"header.{ b64_payload } .sig"
50+
51+
52+ def fake_token_file (exp_days = 3 , refresh_token = "fake_refresh" ):
53+ """Generate fake token file data"""
54+ token = make_fake_jwt (exp_days )
55+ return json .dumps ({"id_token" : token , "refresh_token" : refresh_token })
56+
57+
58+ def fetch_page_side_effect (url ):
59+ """Simulate OpenIdConnect_Utils.fetch_page behavior in tests."""
60+ if url .endswith ("auth_config.json" ):
61+ return True , {
62+ "client_secret" : "secret" ,
63+ "audience" : "https://iam.example.com" ,
64+ "client_id" : "cid" ,
65+ "oidc_config_url" : "https://oidc.example.org/.well-known/openid-configuration" ,
66+ "vo" : "fake_vo" ,
67+ "no_verify" : "True" ,
68+ "robot_ids" : "NONE" ,
69+ }
70+ elif url .endswith ("openid-configuration" ):
71+ return True , {"token_endpoint" : "https://oidc.example.org/token" }
72+ return False , {}
3673
3774
3875class VersionTestCase (unittest .TestCase ):
@@ -46,6 +83,21 @@ def test_version(self):
4683class TestPandaAuthUtils (unittest .TestCase ):
4784 """Simple test of auth utilities."""
4885
86+ def setUp (self ):
87+ self .test_env = {
88+ "PANDA_CONFIG_ROOT" : "/fake/token" ,
89+ "PANDA_URL_SSL" : "https://fake.server.com:8443/server/panda" ,
90+ "PANDA_URL" : "https://fake.server.com:8443/server/panda" ,
91+ "PANDACACHE_URL" : "https://fake.server.com:8443/server/panda" ,
92+ "PANDAMON_URL" : "https://fake.monitor.com:8443/" ,
93+ "PANDA_AUTH" : "oidc" ,
94+ "PANDA_VERIFY_HOST" : "off" ,
95+ "PANDA_AUTH_VO" : "fake_vo" ,
96+ "PANDA_BEHIND_REAL_LB" : "true" ,
97+ "PANDA_SYS" : "/fake/pandasys" ,
98+ "IDDS_CONFIG" : "/fake/pandasys/etc/idds/idds.cfg.client.template" ,
99+ }
100+
49101 def testPandaAuthStatusWrongEnviron (self ):
50102 unwanted = {
51103 "PANDA_AUTH" ,
@@ -59,6 +111,41 @@ def testPandaAuthStatusWrongEnviron(self):
59111 with self .assertRaises (OSError ):
60112 panda_auth_status ()
61113
114+ @mock .patch ("builtins.print" )
115+ @mock .patch ("os.path.exists" , return_value = True )
116+ @mock .patch ("pandaclient.openidc_utils.OpenIdConnect_Utils" )
117+ def test_expired_token (self , mock_oidc , mock_exists , mock_print ):
118+ mock_oidc .return_value .get_token_path .return_value = "/fake/token.json"
119+
120+ with mock .patch .dict ("os.environ" , self .test_env ):
121+ with mock .patch ("builtins.open" , mock .mock_open (read_data = fake_token_file (exp_days = - 1 ))):
122+ with self .assertRaises (TokenExpiredError ):
123+ panda_auth_refresh (days = 4 )
124+
125+ @mock .patch ("builtins.print" )
126+ @mock .patch ("lsst.ctrl.bps.panda.panda_auth_utils.panda_auth_status" )
127+ @mock .patch ("os.path.exists" , return_value = True )
128+ @mock .patch ("lsst.ctrl.bps.panda.panda_auth_utils.OpenIdConnect_Utils" )
129+ def test_successful_refresh (self , mock_oidc , mock_exists , mock_status , mock_print ):
130+ fake_openid = mock_oidc .return_value
131+ fake_openid .get_token_path .return_value = "/fake/token.json"
132+ fake_openid .auth_config_url = "https://fake.server/auth_config.json"
133+
134+ fake_openid .fetch_page .side_effect = fetch_page_side_effect
135+
136+ fake_openid .refresh_token .return_value = (True , {"access_token" : "new_token" })
137+
138+ mock_status .return_value = {"exp" : int ((datetime .now (UTC ) + timedelta (seconds = 3600 )).timestamp ())}
139+
140+ with mock .patch .dict ("os.environ" , self .test_env ):
141+ token_json = fake_token_file (exp_days = 2 )
142+ with mock .patch ("builtins.open" , mock .mock_open (read_data = token_json )):
143+ panda_auth_refresh (days = 4 )
144+
145+ fake_openid .refresh_token .assert_called_once ()
146+ found = any ("Success to refresh token" in str (c [0 ][0 ]) for c in mock_print .call_args_list )
147+ assert found
148+
62149
63150if __name__ == "__main__" :
64151 unittest .main ()
0 commit comments