sysnet-auth 0.2.3__tar.gz → 0.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. {sysnet_auth-0.2.3/sysnet_auth.egg-info → sysnet_auth-0.2.4}/PKG-INFO +12 -12
  2. {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/auth_lib/__init__.py +2 -1
  3. {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/auth_lib/config.py +11 -1
  4. {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/auth_lib/dependencies.py +28 -16
  5. {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/auth_lib/observability.py +3 -3
  6. {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/pyproject.toml +24 -14
  7. {sysnet_auth-0.2.3 → sysnet_auth-0.2.4/sysnet_auth.egg-info}/PKG-INFO +12 -12
  8. sysnet_auth-0.2.4/sysnet_auth.egg-info/requires.txt +14 -0
  9. {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/tests/test_edges.py +0 -3
  10. {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/tests/test_features.py +11 -10
  11. {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/tests/test_jwt.py +12 -7
  12. sysnet_auth-0.2.3/sysnet_auth.egg-info/requires.txt +0 -14
  13. {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/LICENSE +0 -0
  14. {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/README.md +0 -0
  15. {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/auth_lib/exceptions.py +0 -0
  16. {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/auth_lib/jwks.py +0 -0
  17. {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/auth_lib/models.py +0 -0
  18. {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/auth_lib/py.typed +0 -0
  19. {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/auth_lib/roles.py +0 -0
  20. {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/setup.cfg +0 -0
  21. {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/sysnet_auth.egg-info/SOURCES.txt +0 -0
  22. {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/sysnet_auth.egg-info/dependency_links.txt +0 -0
  23. {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/sysnet_auth.egg-info/top_level.txt +0 -0
  24. {sysnet_auth-0.2.3 → sysnet_auth-0.2.4}/tests/test_roles.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sysnet-auth
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Sdilena autentizacni knihovna pro FastAPI mikrosluzby s Keycloack (OIDC/JWT).
5
5
  Author: SYSNET s.r.o.
6
6
  License: GNU Affero General Public License v3
@@ -21,19 +21,19 @@ Classifier: License :: OSI Approved :: GNU Affero General Public License v3
21
21
  Requires-Python: >=3.11
22
22
  Description-Content-Type: text/markdown
23
23
  License-File: LICENSE
24
- Requires-Dist: fastapi>=0.115
25
- Requires-Dist: pydantic>=2.9
26
- Requires-Dist: pydantic-settings>=2.3
27
- Requires-Dist: pyjwt[crypto]>=2.9
28
- Requires-Dist: httpx>=0.27
24
+ Requires-Dist: fastapi<1,>=0.115
25
+ Requires-Dist: pydantic<3,>=2.9
26
+ Requires-Dist: pydantic-settings<3,>=2.3
27
+ Requires-Dist: pyjwt[crypto]<3,>=2.9
28
+ Requires-Dist: httpx<1,>=0.27
29
29
  Requires-Dist: sysnet-pyutils>=0.1
30
30
  Provides-Extra: dev
31
- Requires-Dist: pytest>=8.0; extra == "dev"
32
- Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
33
- Requires-Dist: pytest-cov>=5.0; extra == "dev"
34
- Requires-Dist: cryptography>=42.0; extra == "dev"
35
- Requires-Dist: mypy>=1.11; extra == "dev"
36
- Requires-Dist: ruff>=0.6; extra == "dev"
31
+ Requires-Dist: pytest<10,>=8.0; extra == "dev"
32
+ Requires-Dist: pytest-asyncio<1,>=0.23; extra == "dev"
33
+ Requires-Dist: pytest-cov<8,>=5.0; extra == "dev"
34
+ Requires-Dist: cryptography<48,>=42.0; extra == "dev"
35
+ Requires-Dist: mypy<2,>=1.11; extra == "dev"
36
+ Requires-Dist: ruff<1,>=0.6; extra == "dev"
37
37
  Dynamic: license-file
38
38
 
39
39
  # sysnet-auth (import path: `auth_lib`)
@@ -9,7 +9,8 @@ Verzovani:
9
9
  se cte pres ``importlib.metadata``, aby nedochazelo k drift mezi nimi.
10
10
  """
11
11
 
12
- from importlib.metadata import PackageNotFoundError, version as _pkg_version
12
+ from importlib.metadata import PackageNotFoundError
13
+ from importlib.metadata import version as _pkg_version
13
14
 
14
15
  from auth_lib.config import AuthSettings, get_settings
15
16
  from auth_lib.dependencies import (
@@ -76,6 +76,15 @@ class AuthSettings(BaseSettings):
76
76
  ),
77
77
  )
78
78
 
79
+ verify_keycloak: bool = Field(
80
+ default=True,
81
+ description=(
82
+ "Pokud False, preskoci se overovani podpisu JWT vuci Keycloak JWKS. "
83
+ "Vhodne pro lokalni vyvoj a testovani bez beziciho Keycloaku. "
84
+ "NIKDY nepouzivat v produkci."
85
+ ),
86
+ )
87
+
79
88
  @field_validator("keycloak_url")
80
89
  @classmethod
81
90
  def _strip_trailing_slash(cls, v: str) -> str:
@@ -89,6 +98,7 @@ class AuthSettings(BaseSettings):
89
98
  s = v.strip()
90
99
  if s.startswith("["):
91
100
  import json
101
+
92
102
  try:
93
103
  parsed = json.loads(s)
94
104
  if isinstance(parsed, list):
@@ -111,4 +121,4 @@ class AuthSettings(BaseSettings):
111
121
  @lru_cache(maxsize=1)
112
122
  def get_settings() -> AuthSettings:
113
123
  """Vrati cachovanou instanci settings. Reset pres get_settings.cache_clear()."""
114
- return AuthSettings() # type: ignore[call-arg]
124
+ return AuthSettings()
@@ -61,33 +61,48 @@ async def _decode_token(
61
61
  raise InvalidTokenError(f"Malformed token header: {exc}") from exc
62
62
 
63
63
  kid = header.get("kid")
64
- if not kid:
64
+ if settings.verify_keycloak and not kid:
65
65
  raise InvalidTokenError("Token header missing 'kid'")
66
66
 
67
- jwk = await jwks_client.get_key(kid)
68
- public_key = _jwk_to_public_key(jwk)
67
+ public_key = None
68
+ if settings.verify_keycloak:
69
+ jwk = await jwks_client.get_key(kid)
70
+ public_key = _jwk_to_public_key(jwk)
69
71
 
70
72
  try:
73
+ # Robustní audience: PyJWT neumí nativně list v aud, pokud hledáme string.
74
+ # Vypneme nativní kontrolu a uděláme ji ručně níže.
75
+ decode_options = {
76
+ "require": ["exp", "iat", "iss", "aud", "sub"],
77
+ "verify_signature": settings.verify_keycloak,
78
+ "verify_exp": True,
79
+ "verify_iat": True,
80
+ "verify_iss": True,
81
+ "verify_aud": False, # Manuálně
82
+ }
83
+
71
84
  claims = jwt.decode(
72
85
  token,
73
86
  key=public_key,
74
87
  algorithms=settings.algorithms,
75
- audience=settings.audience,
76
88
  issuer=settings.issuer,
77
89
  leeway=settings.leeway_seconds,
78
- options={
79
- "require": ["exp", "iat", "iss", "aud", "sub"],
80
- "verify_signature": True,
81
- "verify_exp": True,
82
- "verify_iat": True,
83
- "verify_iss": True,
84
- "verify_aud": True,
85
- },
90
+ options=decode_options,
86
91
  )
92
+
93
+ # Manuální kontrola audience (podpora pro list i string)
94
+ token_aud = claims.get("aud")
95
+ target_aud = settings.audience
96
+ if isinstance(token_aud, list):
97
+ if target_aud not in token_aud:
98
+ raise jwt.InvalidAudienceError(f"Audience {target_aud!r} not in token {token_aud!r}")
99
+ elif token_aud != target_aud:
100
+ raise jwt.InvalidAudienceError(f"Audience mismatch: expected {target_aud!r}, got {token_aud!r}")
101
+
87
102
  except jwt.ExpiredSignatureError as exc:
88
103
  raise InvalidTokenError("Token has expired") from exc
89
104
  except jwt.InvalidAudienceError as exc:
90
- raise InvalidTokenError("Invalid token audience") from exc
105
+ raise InvalidTokenError(str(exc)) from exc
91
106
  except jwt.InvalidIssuerError as exc:
92
107
  raise InvalidTokenError("Invalid token issuer") from exc
93
108
  except jwt.InvalidSignatureError as exc:
@@ -101,7 +116,6 @@ async def _decode_token(
101
116
 
102
117
 
103
118
  async def get_current_user(
104
- request: Request,
105
119
  credentials: HTTPAuthorizationCredentials | None = Depends(_bearer_scheme),
106
120
  settings: AuthSettings = Depends(get_settings),
107
121
  ) -> AuthenticatedUser:
@@ -114,8 +128,6 @@ async def get_current_user(
114
128
  Pokud settings.resource_client je nastaveny, role se mergujou z:
115
129
  realm_access.roles + resource_access.<resource_client>.roles
116
130
  """
117
- _ = request
118
-
119
131
  if credentials is None or not credentials.credentials:
120
132
  exc = InvalidTokenError("Missing Authorization bearer token")
121
133
  _emit_rejected(exc)
@@ -56,7 +56,7 @@ def remove_listener(fn: Callable[..., None]) -> None:
56
56
  """Odebere callback ze vsech registru."""
57
57
  for registry in (_on_validated, _on_rejected, _on_jwks_refresh):
58
58
  if fn in registry:
59
- registry.remove(fn) # type: ignore[arg-type]
59
+ registry.remove(fn)
60
60
 
61
61
 
62
62
  def clear_listeners() -> None:
@@ -82,7 +82,7 @@ def install_sysnet_logging() -> None:
82
82
 
83
83
  logger = Log().logger
84
84
 
85
- def _on_ok(user: "AuthenticatedUser") -> None:
85
+ def _on_ok(user: AuthenticatedUser) -> None:
86
86
  logger.info("auth_lib: token validated (sub=%s, roles=%d)", user.sub, len(user.roles))
87
87
 
88
88
  def _on_err(exc: BaseException) -> None:
@@ -104,7 +104,7 @@ def install_sysnet_logging() -> None:
104
104
  # ----- interni emitery (vola knihovna) --------------------------------
105
105
 
106
106
 
107
- def _emit_validated(user: "AuthenticatedUser") -> None:
107
+ def _emit_validated(user: AuthenticatedUser) -> None:
108
108
  for h in list(_on_validated):
109
109
  try:
110
110
  h(user)
@@ -3,9 +3,9 @@ requires = ["setuptools>=68", "wheel"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
- # Distribucni nazev na internim PyPi. Import path zustava ``auth_lib``.
6
+ # Distribucni nazev na internim PyPi. Import path zustava \`\`auth_lib\`\`.
7
7
  name = "sysnet-auth"
8
- version = "0.2.3"
8
+ version = "0.2.4"
9
9
  description = "Sdilena autentizacni knihovna pro FastAPI mikrosluzby s Keycloack (OIDC/JWT)."
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.11"
@@ -27,22 +27,22 @@ classifiers = [
27
27
  ]
28
28
 
29
29
  dependencies = [
30
- "fastapi>=0.115",
31
- "pydantic>=2.9",
32
- "pydantic-settings>=2.3",
33
- "pyjwt[crypto]>=2.9",
34
- "httpx>=0.27",
30
+ "fastapi>=0.115,<1",
31
+ "pydantic>=2.9,<3",
32
+ "pydantic-settings>=2.3,<3",
33
+ "pyjwt[crypto]>=2.9,<3",
34
+ "httpx>=0.27,<1",
35
35
  "sysnet-pyutils>=0.1",
36
36
  ]
37
37
 
38
38
  [project.optional-dependencies]
39
39
  dev = [
40
- "pytest>=8.0",
41
- "pytest-asyncio>=0.23",
42
- "pytest-cov>=5.0",
43
- "cryptography>=42.0",
44
- "mypy>=1.11",
45
- "ruff>=0.6",
40
+ "pytest>=8.0,<10",
41
+ "pytest-asyncio>=0.23,<1",
42
+ "pytest-cov>=5.0,<8",
43
+ "cryptography>=42.0,<48",
44
+ "mypy>=1.11,<2",
45
+ "ruff>=0.6,<1",
46
46
  ]
47
47
 
48
48
  [project.urls]
@@ -60,7 +60,9 @@ exclude = ["tests*"]
60
60
  "auth_lib" = ["py.typed"]
61
61
 
62
62
  [tool.pytest.ini_options]
63
+ # Explicitne vyzadujeme asyncio pro testy
63
64
  asyncio_mode = "auto"
65
+ asyncio_default_fixture_loop_scope = "function"
64
66
  testpaths = ["tests"]
65
67
  addopts = "-ra --strict-markers"
66
68
 
@@ -70,7 +72,7 @@ target-version = "py311"
70
72
 
71
73
  [tool.ruff.lint]
72
74
  select = ["E", "F", "W", "I", "B", "UP", "ASYNC", "S", "SIM"]
73
- ignore = ["S101"] # asserts v testech jsou ok
75
+ ignore = ["S101", "B008"]
74
76
 
75
77
  [tool.ruff.lint.per-file-ignores]
76
78
  "tests/*" = ["S", "B"]
@@ -84,6 +86,10 @@ show_error_codes = true
84
86
  disallow_any_generics = true
85
87
  plugins = ["pydantic.mypy"]
86
88
 
89
+ [[tool.mypy.overrides]]
90
+ module = ["tests.*", "scripts.*"]
91
+ ignore_errors = true
92
+
87
93
  [[tool.mypy.overrides]]
88
94
  module = "jwt.*"
89
95
  ignore_missing_imports = true
@@ -91,3 +97,7 @@ ignore_missing_imports = true
91
97
  [[tool.mypy.overrides]]
92
98
  module = "sysnet_pyutils.*"
93
99
  ignore_missing_imports = true
100
+
101
+ [[tool.mypy.overrides]]
102
+ module = "tomli.*"
103
+ ignore_missing_imports = true
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sysnet-auth
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Sdilena autentizacni knihovna pro FastAPI mikrosluzby s Keycloack (OIDC/JWT).
5
5
  Author: SYSNET s.r.o.
6
6
  License: GNU Affero General Public License v3
@@ -21,19 +21,19 @@ Classifier: License :: OSI Approved :: GNU Affero General Public License v3
21
21
  Requires-Python: >=3.11
22
22
  Description-Content-Type: text/markdown
23
23
  License-File: LICENSE
24
- Requires-Dist: fastapi>=0.115
25
- Requires-Dist: pydantic>=2.9
26
- Requires-Dist: pydantic-settings>=2.3
27
- Requires-Dist: pyjwt[crypto]>=2.9
28
- Requires-Dist: httpx>=0.27
24
+ Requires-Dist: fastapi<1,>=0.115
25
+ Requires-Dist: pydantic<3,>=2.9
26
+ Requires-Dist: pydantic-settings<3,>=2.3
27
+ Requires-Dist: pyjwt[crypto]<3,>=2.9
28
+ Requires-Dist: httpx<1,>=0.27
29
29
  Requires-Dist: sysnet-pyutils>=0.1
30
30
  Provides-Extra: dev
31
- Requires-Dist: pytest>=8.0; extra == "dev"
32
- Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
33
- Requires-Dist: pytest-cov>=5.0; extra == "dev"
34
- Requires-Dist: cryptography>=42.0; extra == "dev"
35
- Requires-Dist: mypy>=1.11; extra == "dev"
36
- Requires-Dist: ruff>=0.6; extra == "dev"
31
+ Requires-Dist: pytest<10,>=8.0; extra == "dev"
32
+ Requires-Dist: pytest-asyncio<1,>=0.23; extra == "dev"
33
+ Requires-Dist: pytest-cov<8,>=5.0; extra == "dev"
34
+ Requires-Dist: cryptography<48,>=42.0; extra == "dev"
35
+ Requires-Dist: mypy<2,>=1.11; extra == "dev"
36
+ Requires-Dist: ruff<1,>=0.6; extra == "dev"
37
37
  Dynamic: license-file
38
38
 
39
39
  # sysnet-auth (import path: `auth_lib`)
@@ -0,0 +1,14 @@
1
+ fastapi<1,>=0.115
2
+ pydantic<3,>=2.9
3
+ pydantic-settings<3,>=2.3
4
+ pyjwt[crypto]<3,>=2.9
5
+ httpx<1,>=0.27
6
+ sysnet-pyutils>=0.1
7
+
8
+ [dev]
9
+ pytest<10,>=8.0
10
+ pytest-asyncio<1,>=0.23
11
+ pytest-cov<8,>=5.0
12
+ cryptography<48,>=42.0
13
+ mypy<2,>=1.11
14
+ ruff<1,>=0.6
@@ -13,8 +13,6 @@ Pokryva:
13
13
  from __future__ import annotations
14
14
 
15
15
  import json
16
- import types
17
- from typing import Any
18
16
 
19
17
  import httpx
20
18
  import pytest
@@ -26,7 +24,6 @@ from auth_lib.config import AuthSettings
26
24
  from auth_lib.dependencies import _jwk_to_public_key, install_exception_handlers
27
25
  from auth_lib.jwks import JWKSClient, reset_jwks_client
28
26
 
29
-
30
27
  # ----- _fetch happy path + malformed --------------------------------
31
28
 
32
29
 
@@ -13,7 +13,6 @@ from __future__ import annotations
13
13
 
14
14
  import time
15
15
  import types
16
- from typing import Any
17
16
 
18
17
  import httpx
19
18
  import pytest
@@ -35,7 +34,6 @@ from auth_lib import (
35
34
  )
36
35
  from auth_lib import observability as obs
37
36
 
38
-
39
37
  # ---------- Observability hooks ----------
40
38
 
41
39
 
@@ -115,6 +113,7 @@ def test_install_sysnet_logging_is_idempotent():
115
113
  def test_jwks_refresh_hook_fires(test_settings, public_jwk):
116
114
  """Fire kdyz _fetch uspesne dobehne."""
117
115
  import json
116
+
118
117
  clear_listeners()
119
118
  captured: list[int] = []
120
119
 
@@ -132,6 +131,7 @@ def test_jwks_refresh_hook_fires(test_settings, public_jwk):
132
131
  http = httpx.AsyncClient(transport=_T())
133
132
 
134
133
  import asyncio
134
+
135
135
  async def run():
136
136
  c = JWKSClient(test_settings, http_client=http)
137
137
  await c.refresh()
@@ -194,9 +194,10 @@ def test_install_exception_handlers_returns_error_model_shape():
194
194
  def test_resource_client_merges_client_roles(public_jwk, private_pem):
195
195
  """get_current_user pri AUTH_RESOURCE_CLIENT=my-api mergne client role."""
196
196
  import jwt as pyjwt
197
+
198
+ from auth_lib import jwks as jwks_mod
197
199
  from auth_lib.config import AuthSettings
198
200
  from auth_lib.dependencies import get_current_user
199
- from auth_lib import jwks as jwks_mod
200
201
  from auth_lib.jwks import JWKSClient
201
202
 
202
203
  settings = AuthSettings(
@@ -238,6 +239,7 @@ def test_resource_client_merges_client_roles(public_jwk, private_pem):
238
239
  # novy prepsal po ni. Obnovim do puvodniho pri exitu.
239
240
  try:
240
241
  from auth_lib.config import get_settings
242
+
241
243
  app.dependency_overrides[get_settings] = lambda: settings
242
244
  c = TestClient(app)
243
245
  r = c.get("/who", headers={"Authorization": f"Bearer {token}"})
@@ -265,7 +267,7 @@ async def test_kid_miss_cooldown_refreshes_when_enabled(public_jwk):
265
267
  # Fresh cache ale bez hledaneho kidu
266
268
  c._keys_by_kid = {"old-kid": {"kid": "old-kid"}}
267
269
  c._expires_at = time.monotonic() + 3600
268
- c._last_refresh_at = 0.0 # cooldown uplynul
270
+ c._last_refresh_at = -9999.0 # cooldown uplynul (zarucene na jakemkoliv systemu)
269
271
 
270
272
  fetch_calls: list[int] = [0]
271
273
  new_kid = public_jwk["kid"]
@@ -343,6 +345,7 @@ def test_emit_rejected_hook_error_is_swallowed(client, token_factory):
343
345
  def test_emit_jwks_refresh_hook_error_is_swallowed(test_settings, public_jwk):
344
346
  import asyncio
345
347
  import json
348
+
346
349
  clear_listeners()
347
350
 
348
351
  @on_jwks_refresh
@@ -354,6 +357,7 @@ def test_emit_jwks_refresh_hook_error_is_swallowed(test_settings, public_jwk):
354
357
  return httpx.Response(200, content=json.dumps({"keys": [public_jwk]}).encode())
355
358
 
356
359
  from auth_lib.jwks import JWKSClient
360
+
357
361
  http = httpx.AsyncClient(transport=_T())
358
362
 
359
363
  async def run():
@@ -371,9 +375,9 @@ async def test_get_jwks_client_singleton_single_flight():
371
375
  """Overime, ze paralelni volani vrati tentyz singleton."""
372
376
  import asyncio
373
377
 
374
- from auth_lib.jwks import get_jwks_client, reset_jwks_client
375
378
  from auth_lib import jwks as jwks_mod
376
- from auth_lib.config import get_settings, AuthSettings
379
+ from auth_lib.config import AuthSettings, get_settings
380
+ from auth_lib.jwks import get_jwks_client, reset_jwks_client
377
381
 
378
382
  # Nastavime settings pro proces
379
383
  get_settings.cache_clear()
@@ -382,15 +386,12 @@ async def test_get_jwks_client_singleton_single_flight():
382
386
 
383
387
  # Potrebujeme env pro AuthSettings() - prepiseme cache direct
384
388
  settings = AuthSettings(keycloak_url="https://kc", realm="r", audience="a")
385
- from functools import lru_cache
386
389
  original = get_settings
387
390
  try:
388
391
  # Prepiseme lru_cache - vratime stejny settings
389
392
  jwks_mod.get_settings = lambda: settings
390
393
 
391
- results = await asyncio.gather(
392
- get_jwks_client(), get_jwks_client(), get_jwks_client()
393
- )
394
+ results = await asyncio.gather(get_jwks_client(), get_jwks_client(), get_jwks_client())
394
395
  assert all(r is results[0] for r in results)
395
396
  finally:
396
397
  jwks_mod.get_settings = original
@@ -86,7 +86,10 @@ def test_malformed_token_returns_401(client: TestClient) -> None:
86
86
 
87
87
 
88
88
  @pytest.mark.asyncio
89
- async def test_jwks_cache_fetches_once_under_concurrency(mocked_jwks_client, public_jwk: dict) -> None:
89
+ async def test_jwks_cache_fetches_once_under_concurrency(
90
+ mocked_jwks_client,
91
+ public_jwk: dict,
92
+ ) -> None:
90
93
  """Single-flight chovani JWKS cache - 50 soubeznych volani = 1 fetch."""
91
94
  import asyncio
92
95
  import time as _time
@@ -114,10 +117,11 @@ async def test_jwks_cache_fetches_once_under_concurrency(mocked_jwks_client, pub
114
117
  @pytest.mark.asyncio
115
118
  async def test_jwks_kid_missing_after_refresh_is_invalid_token(test_settings, public_jwk) -> None:
116
119
  """Stale cache, refresh doplni cache, ale bez hledaneho kidu -> InvalidTokenError."""
117
- import types
118
120
  import time as _time
119
- from auth_lib.jwks import JWKSClient
121
+ import types
122
+
120
123
  from auth_lib.exceptions import InvalidTokenError
124
+ from auth_lib.jwks import JWKSClient
121
125
 
122
126
  client = JWKSClient(test_settings)
123
127
 
@@ -136,11 +140,11 @@ async def test_jwks_kid_missing_after_refresh_is_invalid_token(test_settings, pu
136
140
  @pytest.mark.asyncio
137
141
  async def test_jwks_refresh_network_error_maps_to_config_error(test_settings) -> None:
138
142
  """Fetch selhe -> AuthConfigurationError (500)."""
139
- import types
140
- from auth_lib.jwks import JWKSClient
141
- from auth_lib.exceptions import AuthConfigurationError
142
143
  import httpx
143
144
 
145
+ from auth_lib.exceptions import AuthConfigurationError
146
+ from auth_lib.jwks import JWKSClient
147
+
144
148
  client = JWKSClient(test_settings)
145
149
 
146
150
  async def broken_get(url):
@@ -157,8 +161,9 @@ async def test_jwks_refresh_network_error_maps_to_config_error(test_settings) ->
157
161
  @pytest.mark.asyncio
158
162
  async def test_jwks_refresh_invalidates_and_reloads(mocked_jwks_client, public_jwk: dict) -> None:
159
163
  """invalidate() + refresh() -> nova data v cache."""
160
- import types
161
164
  import time as _time
165
+ import types
166
+
162
167
  from tests.conftest import TEST_KID
163
168
 
164
169
  async def fake_fetch(self) -> None:
@@ -1,14 +0,0 @@
1
- fastapi>=0.115
2
- pydantic>=2.9
3
- pydantic-settings>=2.3
4
- pyjwt[crypto]>=2.9
5
- httpx>=0.27
6
- sysnet-pyutils>=0.1
7
-
8
- [dev]
9
- pytest>=8.0
10
- pytest-asyncio>=0.23
11
- pytest-cov>=5.0
12
- cryptography>=42.0
13
- mypy>=1.11
14
- ruff>=0.6
File without changes
File without changes
File without changes