@@ -175,11 +175,55 @@ def __repr__(self):
175175 enabled_check = None ,
176176)
177177
178+ LINK_APP_DEF = _FakeOAuthProviderDefinition (
179+ name = "link_app" ,
180+ display_name = "Link App" ,
181+ icon = "link_app" ,
182+ authorize_url = "https://linkapp.test/CNS/oauth2/authorize" ,
183+ authorize_method = "GET" ,
184+ authorize_params = {"response_type" : "code" , "scope" : "read write" },
185+ authorize_fragment = "" ,
186+ authorize_param_map = {
187+ "client_id" : "client_id" ,
188+ "redirect_uri" : "redirect_uri" ,
189+ "scope" : "scope" ,
190+ "state" : "state" ,
191+ },
192+ encode_redirect_uri = False ,
193+ token_url = "https://linkapp.test/CNS/oauth2/token" ,
194+ token_method = "POST" ,
195+ token_params_map = {
196+ "client_id" : "client_id" ,
197+ "client_secret" : "client_secret" ,
198+ "code" : "code" ,
199+ "grant_type" : "grant_type" ,
200+ "redirect_uri" : "redirect_uri" ,
201+ },
202+ token_extra_params = {},
203+ token_error_key = "error" ,
204+ token_error_message_key = "error_description" ,
205+ token_response_id_key = None ,
206+ userinfo_url = "https://linkapp.test/BGM/deparment/syncDept" ,
207+ userinfo_auth_scheme = "Bearer" ,
208+ userinfo_params = {},
209+ userinfo_field_map = {
210+ "id" : "id" ,
211+ "email" : "email" ,
212+ "username" : "login" ,
213+ },
214+ userinfo_needs_email_fetch = False ,
215+ userinfo_email_url = None ,
216+ client_id_env = "LINK_APP_OAUTH_CLIENT_ID" ,
217+ client_secret_env = "LINK_APP_OAUTH_CLIENT_SECRET" ,
218+ enabled_check = None ,
219+ )
220+
178221oauth_providers_mock = MagicMock ()
179222oauth_providers_mock .OAUTH_PROVIDER_REGISTRY = {
180223 "github" : GITHUB_DEF ,
181224 "wechat" : WECHAT_DEF ,
182225 "gde" : GDE_DEF ,
226+ "link_app" : LINK_APP_DEF ,
183227}
184228
185229
@@ -271,7 +315,6 @@ def test_returns_none_for_missing_nested_field(self):
271315 result = _resolve_field (data , "attributes.userId" )
272316 self .assertIsNone (result )
273317
274-
275318class TestBuildSSLContext (unittest .TestCase ):
276319 def test_returns_default_context_when_verify_enabled (self ):
277320 ctx = _build_ssl_context ()
@@ -287,14 +330,22 @@ def test_returns_no_verify_context_when_disabled(self):
287330class TestGetSupportedProviders (unittest .TestCase ):
288331 def test_supported_providers_set (self ):
289332 providers = get_supported_providers ()
290- self .assertEqual (providers , {"github" , "wechat" , "gde" })
333+ self .assertEqual (providers , {"github" , "wechat" , "gde" , "link_app" })
291334
292335
293336class TestGetEnabledProviders (unittest .TestCase ):
294337 def test_returns_github_when_configured (self ):
295338 with patch .dict (
296339 os .environ ,
297- {"GITHUB_OAUTH_CLIENT_ID" : "id" , "GITHUB_OAUTH_CLIENT_SECRET" : "secret" },
340+ {
341+ "GITHUB_OAUTH_CLIENT_ID" : "id" ,
342+ "GITHUB_OAUTH_CLIENT_SECRET" : "secret" ,
343+ "GDE_OAUTH_CLIENT_ID" : "" ,
344+ "GDE_OAUTH_CLIENT_SECRET" : "" ,
345+ "LINK_APP_OAUTH_CLIENT_ID" : "" ,
346+ "LINK_APP_OAUTH_CLIENT_SECRET" : "" ,
347+ "ENABLE_WECHAT_OAUTH" : "false" ,
348+ },
298349 clear = False ,
299350 ):
300351 providers = get_enabled_providers ()
@@ -309,6 +360,10 @@ def test_returns_empty_when_nothing_configured(self):
309360 for k in [
310361 "GITHUB_OAUTH_CLIENT_ID" ,
311362 "GITHUB_OAUTH_CLIENT_SECRET" ,
363+ "GDE_OAUTH_CLIENT_ID" ,
364+ "GDE_OAUTH_CLIENT_SECRET" ,
365+ "LINK_APP_OAUTH_CLIENT_ID" ,
366+ "LINK_APP_OAUTH_CLIENT_SECRET" ,
312367 "WECHAT_OAUTH_APP_ID" ,
313368 "WECHAT_OAUTH_APP_SECRET" ,
314369 ]
@@ -326,6 +381,10 @@ def test_returns_both_when_all_configured(self):
326381 "ENABLE_WECHAT_OAUTH" : "true" ,
327382 "WECHAT_OAUTH_APP_ID" : "wx_id" ,
328383 "WECHAT_OAUTH_APP_SECRET" : "wx_secret" ,
384+ "GDE_OAUTH_CLIENT_ID" : "" ,
385+ "GDE_OAUTH_CLIENT_SECRET" : "" ,
386+ "LINK_APP_OAUTH_CLIENT_ID" : "" ,
387+ "LINK_APP_OAUTH_CLIENT_SECRET" : "" ,
329388 }
330389 with patch .dict (os .environ , env , clear = False ):
331390 providers = get_enabled_providers ()
@@ -343,6 +402,10 @@ def test_returns_github_authorize_url(self):
343402 {
344403 "GITHUB_OAUTH_CLIENT_ID" : "gh_test_id" ,
345404 "GITHUB_OAUTH_CLIENT_SECRET" : "gh_test_secret" ,
405+ "GDE_OAUTH_CLIENT_ID" : "" ,
406+ "GDE_OAUTH_CLIENT_SECRET" : "" ,
407+ "LINK_APP_OAUTH_CLIENT_ID" : "" ,
408+ "LINK_APP_OAUTH_CLIENT_SECRET" : "" ,
346409 },
347410 clear = False ,
348411 ):
@@ -359,6 +422,10 @@ def test_returns_github_authorize_url_with_link_user_id(self):
359422 {
360423 "GITHUB_OAUTH_CLIENT_ID" : "gh_test_id" ,
361424 "GITHUB_OAUTH_CLIENT_SECRET" : "gh_test_secret" ,
425+ "GDE_OAUTH_CLIENT_ID" : "" ,
426+ "GDE_OAUTH_CLIENT_SECRET" : "" ,
427+ "LINK_APP_OAUTH_CLIENT_ID" : "" ,
428+ "LINK_APP_OAUTH_CLIENT_SECRET" : "" ,
362429 },
363430 clear = False ,
364431 ):
@@ -380,6 +447,20 @@ def test_returns_wechat_authorize_url(self):
380447 self .assertIn ("appid=wx_test_id" , url )
381448 self .assertTrue (url .endswith ("#wechat_redirect" ))
382449
450+ def test_returns_link_app_authorize_url (self ):
451+ env = {
452+ "LINK_APP_OAUTH_CLIENT_ID" : "link_client" ,
453+ "LINK_APP_OAUTH_CLIENT_SECRET" : "link_secret" ,
454+ }
455+ with patch .dict (os .environ , env , clear = False ):
456+ url = get_authorize_url ("link_app" )
457+
458+ self .assertIn ("linkapp.test/CNS/oauth2/authorize" , url )
459+ self .assertIn ("client_id=link_client" , url )
460+ self .assertIn ("response_type=code" , url )
461+ self .assertIn ("scope=read+write" , url )
462+ self .assertIn ("state=link_app" , url )
463+
383464 def test_unsupported_provider_raises (self ):
384465 with self .assertRaises (_OAuthProviderError ):
385466 get_authorize_url ("google" )
@@ -405,7 +486,6 @@ def test_raises_for_unsupported_provider(self):
405486 with self .assertRaises (_OAuthProviderError ):
406487 get_provider_user_info ("google" , "token123" )
407488
408-
409489class TestCreateOrUpdateOAuthAccount (unittest .TestCase ):
410490 def test_creates_new_account_when_none_exists (self ):
411491 oauth_account_db_mock .reset_mock ()
@@ -699,14 +779,14 @@ def test_returns_email_from_primary_in_emails_list(self):
699779 mock_user_resp .read .return_value = b'{"id": "12345", "login": "octocat"}'
700780 mock_emails_resp = MagicMock ()
701781 mock_emails_resp .read .return_value = b'[{"email": "secondary@github.com", "primary": false}, {"email": "primary@github.com", "primary": true}]'
702-
782+
703783 mock_cm1 = MagicMock ()
704784 mock_cm1 .__enter__ = MagicMock (return_value = mock_user_resp )
705785 mock_cm1 .__exit__ = MagicMock (return_value = False )
706786 mock_cm2 = MagicMock ()
707787 mock_cm2 .__enter__ = MagicMock (return_value = mock_emails_resp )
708788 mock_cm2 .__exit__ = MagicMock (return_value = False )
709-
789+
710790 with patch ("urllib.request.urlopen" , side_effect = [mock_cm1 , mock_cm2 ]):
711791 env = {
712792 "GITHUB_OAUTH_CLIENT_ID" : "id" ,
@@ -722,14 +802,14 @@ def test_returns_first_email_when_no_primary(self):
722802 mock_user_resp .read .return_value = b'{"id": "12345", "login": "octocat"}'
723803 mock_emails_resp = MagicMock ()
724804 mock_emails_resp .read .return_value = b'[{"email": "first@github.com"}]'
725-
805+
726806 mock_cm1 = MagicMock ()
727807 mock_cm1 .__enter__ = MagicMock (return_value = mock_user_resp )
728808 mock_cm1 .__exit__ = MagicMock (return_value = False )
729809 mock_cm2 = MagicMock ()
730810 mock_cm2 .__enter__ = MagicMock (return_value = mock_emails_resp )
731811 mock_cm2 .__exit__ = MagicMock (return_value = False )
732-
812+
733813 with patch ("urllib.request.urlopen" , side_effect = [mock_cm1 , mock_cm2 ]):
734814 env = {
735815 "GITHUB_OAUTH_CLIENT_ID" : "id" ,
@@ -745,14 +825,14 @@ def test_fallback_email_when_no_email_found(self):
745825 mock_user_resp .read .return_value = b'{"id": "12345", "login": "testuser"}'
746826 mock_emails_resp = MagicMock ()
747827 mock_emails_resp .read .return_value = b'[]'
748-
828+
749829 mock_cm1 = MagicMock ()
750830 mock_cm1 .__enter__ = MagicMock (return_value = mock_user_resp )
751831 mock_cm1 .__exit__ = MagicMock (return_value = False )
752832 mock_cm2 = MagicMock ()
753833 mock_cm2 .__enter__ = MagicMock (return_value = mock_emails_resp )
754834 mock_cm2 .__exit__ = MagicMock (return_value = False )
755-
835+
756836 with patch ("urllib.request.urlopen" , side_effect = [mock_cm1 , mock_cm2 ]):
757837 env = {
758838 "GITHUB_OAUTH_CLIENT_ID" : "id" ,
@@ -766,11 +846,11 @@ def test_fallback_email_when_no_email_found(self):
766846 def test_wechat_does_not_fetch_emails (self ):
767847 mock_user_resp = MagicMock ()
768848 mock_user_resp .read .return_value = b'{"openid": "wx123", "nickname": "wechat_user"}'
769-
849+
770850 mock_cm = MagicMock ()
771851 mock_cm .__enter__ = MagicMock (return_value = mock_user_resp )
772852 mock_cm .__exit__ = MagicMock (return_value = False )
773-
853+
774854 with patch ("urllib.request.urlopen" , return_value = mock_cm ):
775855 env = {
776856 "ENABLE_WECHAT_OAUTH" : "true" ,
@@ -786,11 +866,11 @@ def test_wechat_does_not_fetch_emails(self):
786866 def test_resolves_nested_field_path (self ):
787867 mock_user_resp = MagicMock ()
788868 mock_user_resp .read .return_value = b'{"attributes": {"userId": "nested123"}, "id": "testuser"}'
789-
869+
790870 mock_cm = MagicMock ()
791871 mock_cm .__enter__ = MagicMock (return_value = mock_user_resp )
792872 mock_cm .__exit__ = MagicMock (return_value = False )
793-
873+
794874 with patch ("urllib.request.urlopen" , return_value = mock_cm ):
795875 env = {
796876 "GDE_URL" : "https://gde.test" ,
@@ -807,11 +887,11 @@ class TestExchangeCodeForProviderTokenWithMock(unittest.TestCase):
807887 def test_exchange_with_post_method (self ):
808888 mock_token_resp = MagicMock ()
809889 mock_token_resp .read .return_value = b'{"access_token": "gh_token_123"}'
810-
890+
811891 mock_cm = MagicMock ()
812892 mock_cm .__enter__ = MagicMock (return_value = mock_token_resp )
813893 mock_cm .__exit__ = MagicMock (return_value = False )
814-
894+
815895 with patch ("urllib.request.urlopen" , return_value = mock_cm ):
816896 env = {
817897 "GITHUB_OAUTH_CLIENT_ID" : "test_id" ,
@@ -825,11 +905,11 @@ def test_exchange_with_post_method(self):
825905 def test_exchange_with_get_method (self ):
826906 mock_token_resp = MagicMock ()
827907 mock_token_resp .read .return_value = b'{"access_token": "wx_token_456", "openid": "wx_openid"}'
828-
908+
829909 mock_cm = MagicMock ()
830910 mock_cm .__enter__ = MagicMock (return_value = mock_token_resp )
831911 mock_cm .__exit__ = MagicMock (return_value = False )
832-
912+
833913 with patch ("urllib.request.urlopen" , return_value = mock_cm ):
834914 env = {
835915 "ENABLE_WECHAT_OAUTH" : "true" ,
@@ -845,11 +925,11 @@ def test_exchange_with_get_method(self):
845925 def test_raises_on_provider_error_response (self ):
846926 mock_token_resp = MagicMock ()
847927 mock_token_resp .read .return_value = b'{"errcode": 40001, "errmsg": "invalid code"}'
848-
928+
849929 mock_cm = MagicMock ()
850930 mock_cm .__enter__ = MagicMock (return_value = mock_token_resp )
851931 mock_cm .__exit__ = MagicMock (return_value = False )
852-
932+
853933 with patch ("urllib.request.urlopen" , return_value = mock_cm ):
854934 env = {
855935 "ENABLE_WECHAT_OAUTH" : "true" ,
0 commit comments