diff --git a/CHANGELOG.md b/CHANGELOG.md index b3a372e..8103a37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,18 +10,6 @@ is documented in `VERSIONING.md`. ## [Unreleased] -### Changed - -- Release workflow now publishes to PyPI via **Trusted Publishing (OIDC)** - instead of the `PYPI_API_TOKEN` fallback that shipped `v1.2.0`. Added - `id-token: write` / `attestations: write` permissions to the publish - job and removed the explicit `password:` argument. The `environment: - pypi` gate is preserved. PEP 740 attestations are produced for every - artifact, which were disabled under the token path. -- Bumped `actions/download-artifact` from `v5` (Node 20) to `v8` - (Node 24) to clear the runner deprecation annotation. All other - actions already run on Node 24. - ## [1.2.0] - 2026-04-21 ## [1.1.3] - 2026-04-21 diff --git a/COMPATIBILITY_CHECKLIST.md b/COMPATIBILITY_CHECKLIST.md index e2e6ef1..dba5c68 100644 --- a/COMPATIBILITY_CHECKLIST.md +++ b/COMPATIBILITY_CHECKLIST.md @@ -59,8 +59,9 @@ 근거: [python/jwt_rs/api_jws.py](/home/statpan/workspace/pypi_lib/pyjwt-rs/python/jwt_rs/api_jws.py:153) - [x] detached payload (`b64=False`) 지원 근거: [python/jwt_rs/api_jws.py](/home/statpan/workspace/pypi_lib/pyjwt-rs/python/jwt_rs/api_jws.py:148) -- [-] `json_encoder` 완전 호환 - `json.dumps(..., cls=json_encoder)` 는 지원하지만 upstream의 전체 커스텀 알고리즘/객체 조합까지 검증하진 않음 +- [x] `json_encoder` 호환 + payload / header 모두 `json.dumps(..., cls=json_encoder)` 경로를 사용하며, + datetime claim 변환 이후의 payload 값과 비표준 header 값에 대한 회귀 테스트를 추가해 동작을 고정함 ## 4. Decode / Validation Behavior diff --git a/tests/test_upstream_api_jws.py b/tests/test_upstream_api_jws.py index b4b76db..9abd456 100644 --- a/tests/test_upstream_api_jws.py +++ b/tests/test_upstream_api_jws.py @@ -842,6 +842,41 @@ def default(self, o: object) -> str: assert "some_decimal" in header assert header["some_decimal"] == "it worked" + def test_custom_json_encoder_handles_nonstandard_header_values( + self, jws: PyJWS, payload: bytes + ) -> None: + class CustomJSONEncoder(json.JSONEncoder): + def default(self, o: object) -> list[int]: + assert isinstance(o, set) + return sorted(o) + + token = jws.encode( + payload, + HS256_SECRET, + headers={"x-custom": {3, 1, 2}}, + json_encoder=CustomJSONEncoder, + ) + + header, *_ = token.split(".") + header = json.loads(base64url_decode(header)) + + assert header["x-custom"] == [1, 2, 3] + + def test_custom_json_encoder_does_not_bypass_kid_validation( + self, jws: PyJWS, payload: bytes + ) -> None: + class CustomJSONEncoder(json.JSONEncoder): + def default(self, o: object) -> str: + return "serialized" + + with pytest.raises(InvalidTokenError, match="Key ID header parameter must be a string"): + jws.encode( + payload, + HS256_SECRET, + headers={"kid": object()}, + json_encoder=CustomJSONEncoder, + ) + def test_encode_headers_parameter_adds_headers( self, jws: PyJWS, payload: bytes ) -> None: diff --git a/tests/test_upstream_api_jwt.py b/tests/test_upstream_api_jwt.py index 555ca63..28f7594 100644 --- a/tests/test_upstream_api_jwt.py +++ b/tests/test_upstream_api_jwt.py @@ -733,6 +733,25 @@ def default(self, o: object) -> str: assert payload == {"some_decimal": "it worked"} + def test_custom_json_encoder_handles_nested_values_after_datetime_conversion( + self, jwt: PyJWT + ) -> None: + class CustomJSONEncoder(json.JSONEncoder): + def default(self, o: object) -> list[int]: + assert isinstance(o, set) + return sorted(o) + + data = { + "exp": datetime.now(tz=timezone.utc) + timedelta(minutes=5), + "values": {3, 1, 2}, + } + + token = jwt.encode(data, HS256_SECRET, json_encoder=CustomJSONEncoder) + payload = jwt.decode(token, HS256_SECRET, algorithms=["HS256"]) + + assert isinstance(payload["exp"], int) + assert payload["values"] == [1, 2, 3] + def test_decode_with_verify_exp_option( self, jwt: PyJWT, payload: dict[str, object] ) -> None: