Skip to content

updating documentation and refresh the cache#432

Open
Bala-Sakabattula wants to merge 5 commits intorelease-engineering:uat-instancefrom
Bala-Sakabattula:oauth-fix
Open

updating documentation and refresh the cache#432
Bala-Sakabattula wants to merge 5 commits intorelease-engineering:uat-instancefrom
Bala-Sakabattula:oauth-fix

Conversation

@Bala-Sakabattula
Copy link
Collaborator

Updated the documentation and Refresh the cache .

@qodo-code-review
Copy link

Review Summary by Qodo

Add OAuth 2.0 authentication support with cache invalidation

✨ Enhancement 📝 Documentation

Grey Divider

Walkthroughs

Description
• Add OAuth 2.0 authentication support alongside existing PAT method
• Implement automatic token cache invalidation on auth failures
• Update configuration examples and documentation for both auth methods
• Add comprehensive test coverage for OAuth2 cache invalidation
Diagram
flowchart LR
  A["Auth Config"] -->|"PAT or OAuth2"| B["build_jira_client_kwargs"]
  B -->|"OAuth2"| C["Token Cache"]
  D["JIRAError"] -->|"Invalidate"| E["invalidate_oauth2_cache_for_config"]
  E -->|"Clear Cache"| C
  C -->|"Next Request"| B
Loading

Grey Divider

File Changes

1. sync2jira/jira_auth.py ✨ Enhancement +26/-0

Add OAuth2 cache invalidation function

• Add invalidate_oauth2_cache_for_config() function to clear cached OAuth2 tokens on auth failures
• Function checks auth method and removes token from cache if OAuth2 is configured
• Enables retry logic to fetch fresh tokens after Jira rejects requests

sync2jira/jira_auth.py


2. sync2jira/downstream_issue.py ✨ Enhancement +14/-1

Invalidate OAuth2 cache on Jira errors

• Import new invalidate_oauth2_cache_for_config function
• Call cache invalidation on JIRAError before retry to clear expired/revoked tokens
• Determine jira_instance from issue config and invalidate its OAuth2 cache

sync2jira/downstream_issue.py


3. sync2jira/downstream_pr.py ✨ Enhancement +11/-0

Invalidate OAuth2 cache on Jira errors

• Import invalidate_oauth2_cache_for_config function
• Add cache invalidation logic on JIRAError before retry attempt
• Extract jira_instance from PR config and clear its OAuth2 token cache

sync2jira/downstream_pr.py


View more (5)
4. fedmsg.d/sync2jira.py ⚙️ Configuration changes +9/-1

Update Jira auth config for dual methods

• Replace token_auth with auth_method field supporting 'pat' or 'oauth2'
• Add basic_auth tuple for PAT authentication (email, API token)
• Add commented OAuth2 configuration block with client_id and client_secret

fedmsg.d/sync2jira.py


5. tests/test_main.py 🧪 Tests +32/-1

Add OAuth2 cache invalidation tests

• Import invalidate_oauth2_cache_for_config function
• Add test test_jira_auth_invalidate_oauth2_clears_cache verifying cache invalidation
• Test confirms cache is cleared and next build fetches new token

tests/test_main.py


6. README.md 📝 Documentation +15/-3

Document dual authentication methods

• Update prerequisites to list both PAT and OAuth 2.0 authentication options
• Update configuration example to show auth_method and basic_auth for PAT
• Add commented OAuth2 configuration block in example
• Update JIRA configuration table description to mention both auth methods

README.md


7. docs/source/config-file.rst 📝 Documentation +36/-2

Expand auth configuration documentation

• Replace token_auth with basic_auth in example configuration
• Add detailed PAT authentication section with code example
• Add detailed OAuth 2.0 authentication section with code example and optional token_url
• Clarify that PAT is default when auth_method is omitted

docs/source/config-file.rst


8. docs/source/quickstart.rst 📝 Documentation +23/-4

Update quickstart with auth method options

• Update step 3 to explain both PAT and OAuth 2.0 authentication options
• Replace token_auth with auth_method and basic_auth in PAT example
• Add separate OAuth 2.0 configuration example with client_id and client_secret
• Add note that jira_username is optional for duplicate detection

docs/source/quickstart.rst


Grey Divider

Qodo Logo

@qodo-code-review
Copy link

qodo-code-review bot commented Feb 18, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (2) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Unhandled KeyError on jira_instance📘 Rule violation ⛯ Reliability
Description
In both downstream_issue.py (line 1415) and downstream_pr.py (line 212), the code accesses
config["sync2jira"]["jira"][jira_instance] without guarding against a KeyError if
jira_instance is a non-empty string that doesn't exist as a key in the config. This edge case is
unhandled and would cause an unexpected exception inside an already-active JIRAError handler,
masking the original error.
Code

sync2jira/downstream_issue.py[R1413-1416]

+            if jira_instance:
+                invalidate_oauth2_cache_for_config(
+                    config["sync2jira"]["jira"][jira_instance]
+                )
Evidence
Compliance rule 3 requires all potential failure points to be identified and handled, including edge
cases. jira_instance is populated from issue.downstream.get(...) or the default config value —
both can return a string that is not a valid key in config["sync2jira"]["jira"]. The guard `if
jira_instance:` only checks truthiness, not existence in the dict, so a misconfigured instance name
would raise an unhandled KeyError inside the JIRAError except block.

Rule 3: Generic: Robust Error Handling and Edge Case Management
sync2jira/downstream_issue.py[1413-1416]
sync2jira/downstream_pr.py[210-213]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Accessing `config["sync2jira"]["jira"][jira_instance]` without guarding for `KeyError` is an unhandled edge case. If `jira_instance` holds a value not present in the config dict, a `KeyError` will be raised inside an active `except JIRAError` handler, masking the original error and potentially crashing the service.
## Issue Context
The same pattern exists in both `downstream_issue.py` and `downstream_pr.py`. The fix should use `.get()` or a try/except block and log a warning when the instance is not found.
## Fix Focus Areas
- sync2jira/downstream_issue.py[1409-1416]
- sync2jira/downstream_pr.py[206-213]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. OAuth2 cache invalidated on all JIRAErrors🐞 Bug ✓ Correctness
Description
In both downstream_issue.py and downstream_pr.py, invalidate_oauth2_cache_for_config() is called
unconditionally for any JIRAError, not just authentication failures (HTTP 401/403). This causes
spurious HTTP POSTs to the OAuth2 token endpoint and unnecessary JIRA session() calls on every
non-auth error (e.g., 400, 404, 500), potentially exhausting OAuth2 rate limits under high sync
load.
Code

sync2jira/downstream_issue.py[R1407-1416]

+            # The error may be due to expired/revoked auth. Invalidate OAuth2
+            # cache so the next get_jira_client fetches a new token (no-op for PAT).
+            jira_instance = issue.downstream.get(
+                "jira_instance",
+                config["sync2jira"].get("default_jira_instance"),
+            )
+            if jira_instance:
+                invalidate_oauth2_cache_for_config(
+                    config["sync2jira"]["jira"][jira_instance]
+                )
Evidence
The bare except JIRAError: block at line 1401 catches all JIRA HTTP errors. The new invalidation
code at lines 1407-1416 does not inspect the exception's status_code before calling
invalidate_oauth2_cache_for_config(). In jira_auth.py, _fetch_oauth2_token() (lines 102-108) makes
an HTTP POST to the OAuth2 token endpoint every time the cache is empty, and get_jira_client()
(downstream_issue.py line 299) calls client.session() — an additional network round-trip. The
identical pattern exists in downstream_pr.py lines 204-213. Only HTTP 401/403 indicate an
expired/revoked token; all other errors (400, 404, 500) do not warrant cache invalidation.

sync2jira/downstream_issue.py[1401-1416]
sync2jira/downstream_pr.py[197-213]
sync2jira/jira_auth.py[96-108]
sync2jira/downstream_issue.py[297-299]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The OAuth2 token cache is invalidated unconditionally inside `except JIRAError:` blocks in both `downstream_issue.py` and `downstream_pr.py`. This means any Jira error (400 Bad Request, 404 Not Found, 500 Internal Server Error, etc.) triggers a spurious HTTP POST to the OAuth2 token endpoint and a new JIRA `client.session()` call. Only HTTP 401 (Unauthorized) and 403 (Forbidden) indicate that the OAuth2 token is invalid and the cache should be cleared.
## Issue Context
The `JIRAError` exception from the `jira` library exposes a `status_code` attribute. The cache invalidation and client refresh should only happen when the error is authentication-related. For all other errors, the existing client and token are still valid — the retry should reuse them.
## Fix Focus Areas
- `sync2jira/downstream_issue.py[1401-1424]` — add `if exc.status_code in (401, 403):` guard before calling `invalidate_oauth2_cache_for_config`; capture the exception as `exc`
- `sync2jira/downstream_pr.py[197-220]` — same fix in the PR sync retry block

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Cache invalidation missing instance context 📘 Rule violation ✓ Correctness
Description
The log.debug call at line 183 of jira_auth.py does not include which Jira instance's token
cache was invalidated, making it impossible to reconstruct which instance experienced an auth
failure during an audit. This violates the requirement that each log entry includes a clear
description of the action and sufficient context to reconstruct the event.
Code

sync2jira/jira_auth.py[183]

+    log.debug("Invalidated OAuth2 token cache for Jira instance")
Evidence
The compliance rule requires each log entry to include sufficient context to reconstruct the event.
The debug log at line 183 only says 'Invalidated OAuth2 token cache for Jira instance' without
identifying which client_id or instance was affected, making it impossible to correlate with a
specific auth failure event.

Rule 1: Generic: Comprehensive Audit Trails
sync2jira/jira_auth.py[183-183]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The debug log for OAuth2 cache invalidation does not include which Jira instance was affected, making it impossible to correlate the event during auditing.
## Issue Context
The `invalidate_oauth2_cache_for_config` function already has access to `client_id` and `token_url`. Including a non-sensitive identifier (e.g., masked `client_id` or the `token_url` domain) in the log message would satisfy audit trail requirements without exposing secrets.
## Fix Focus Areas
- sync2jira/jira_auth.py[176-183]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Test uses unmocked time.time() for cache assertion 🐞 Bug ⛯ Reliability
Description
The new test test_jira_auth_invalidate_oauth2_clears_cache asserts that a second call to
build_jira_client_kwargs() hits the cache (mock_post.assert_called_once()), but time.time() is not
mocked. The cache validity check in _get_oauth2_token depends on real wall-clock time, making the
test implicitly time-dependent and fragile if expires_in values or the
OAUTH2_TOKEN_REFRESH_BUFFER_SECONDS constant change.
Code

tests/test_main.py[R500-511]

+        mock_post.side_effect = [
+            make_response("cached_token"),
+            make_response("new_token_after_invalidate"),
+        ]
+        config = {
+            "options": {"server": "https://site.atlassian.net"},
+            "auth_method": "oauth2",
+            "oauth2": {"client_id": "cid", "client_secret": "csecret"},
+        }
+        build_jira_client_kwargs(config)
+        build_jira_client_kwargs(config)
+        mock_post.assert_called_once()
Evidence
The test calls build_jira_client_kwargs() twice and asserts mock_post.assert_called_once() to prove
the second call used the cache. The cache hit decision in _get_oauth2_token (jira_auth.py lines
99-101) is: if now < entry.expires_at - OAUTH2_TOKEN_REFRESH_BUFFER_SECONDS. Since time.time() is
not mocked, the test relies on real elapsed time being negligible. If expires_in is ever reduced or
OAUTH2_TOKEN_REFRESH_BUFFER_SECONDS is increased in future changes, the assertion could silently
break without any indication of why.

tests/test_main.py[500-511]
sync2jira/jira_auth.py[99-101]
sync2jira/jira_auth.py[51-51]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The test `test_jira_auth_invalidate_oauth2_clears_cache` relies on real `time.time()` to verify that the OAuth2 token cache is hit on the second call. The cache validity logic in `_get_oauth2_token` computes `now &amp;amp;amp;amp;lt; entry.expires_at - OAUTH2_TOKEN_REFRESH_BUFFER_SECONDS`, where `now = time.time()`. Without mocking `time.time()`, the test is implicitly time-dependent.
## Issue Context
The test currently works because `expires_in=3600` and `OAUTH2_TOKEN_REFRESH_BUFFER_SECONDS=300` give a 3300-second valid window, far exceeding the microseconds between the two calls. However, if either constant changes, the test could silently break. Mocking `time.time()` makes the test robust and self-documenting.
## Fix Focus Areas
- `tests/test_main.py[489-515]` — add `@patch(&amp;amp;amp;amp;#x27;sync2jira.jira_auth.time.time&amp;amp;amp;amp;#x27;, return_value=1_000_000)` decorator to `test_jira_auth_invalidate_oauth2_clears_cache` and accept the mock as a parameter

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

webbnh

This comment was marked as resolved.

@webbnh
Copy link
Collaborator

webbnh commented Feb 19, 2026

Speaking of nits and code quality suggestions, both of Qodo's "Review recommended" comments above are valid:

  • the Debug log message probably should either be improved or removed;
  • the unmocked time.time() call could be a problem, though I doubt it ever will be, but in terms of being thorough we should probably mock it.

webbnh

This comment was marked as resolved.

@Bala-Sakabattula
Copy link
Collaborator Author

@webbnh, I’ve added a test case to cover the new logic in the get_jira_client function when invalidate_oauth2_cache is set to True.

On the other hand, there is no unit test at all for that function

Could you please clarify which function you are referring to? In test_main, we already have a test case for invalidate_oauth2_cache_for_config, and there are existing test cases for get_jira_client as well.

Copy link
Collaborator

@webbnh webbnh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ve added a test case to cover the new logic in the get_jira_client function when invalidate_oauth2_cache is set to True.

Excellent! Thank you!

On the other hand, there is no unit test at all for that function

Could you please clarify which function you are referring to? In test_main, we already have a test case for invalidate_oauth2_cache_for_config, and there are existing test cases for get_jira_client as well.

You are correct, I was mistaken. However, this results in a mystery: one of those tests should be driving coverage for a branch which is being reported as not covered. If you're game, it would be good to fix that test (I can see why it isn't driving the coverage, but I'm not seeing how it is passing.)

Copy link
Collaborator

@webbnh webbnh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Collaborator

@webbnh webbnh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you doing what you think you're doing? 🙂

"oauth2": "invalid", # not a dict, so branch around token fetch is taken
}
with self.assertRaises(ValueError) as ctx:
build_jira_client_kwargs(config)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this test supposed to be targeting invalidate_oauth2_cache_for_config() rather than build_jira_client_kwargs()?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it is not raising any exception to be checked just normal return and also in the following test case in this line we are checking the calling of invalidate_oauth2_cache_for_config() with invalid oauth2. We don't have exception to check so we are asserting that we are calling that function.

https://github.com/release-engineering/Sync2Jira/pull/432/changes#diff-b2c0f23b6e1e250652284cfceedbdb02d7e8af1b0ba5bbb7f83d79bd56a61f0bR242

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normally, when we give a function bad inputs, it reports an error, and so we devise a test which supplies such inputs and ensures that the error occurs. However, in this case, the CUT is intended to report no errors when we give it bad inputs, and so we should have a test which ensures that no exception is raised when the bad inputs are supplied. And, as you've observed, such a test would not include any explicit assertions, because none are required (and, in fact, none can be made); it would rely instead on the test framework reporting a failure if any error were "unexpectedly" raised, because, that, in fact, would be the case.

@Bala-Sakabattula
Copy link
Collaborator Author

Just to clarify whether we want to explicitly test the following condition:

oauth2_cfg = jira_instance_config.get("oauth2")
if not oauth2_cfg or not isinstance(oauth2_cfg, dict):
    return

In this case, the function simply returns early and does not raise an exception or produce any observable behavior.

If we add a test for this, it would look something like:

def test_jira_auth_invalidate_oauth2_no_oauth2_or_not_dict(self):
    """invalidate_oauth2_cache_for_config returns early when oauth2 is missing or not a dict."""
    for config in [
        {},
        {"options": {}},
        {"oauth2": None},
        {"oauth2": "not_a_dict"},
    ]:
        invalidate_oauth2_cache_for_config(config)

However, since there is no exception or return value to assert, the test would mainly ensure that the function executes without errors (essentially for coverage). In the tagged test case as well, we are only asserting that the function is called.

Just checking if adding this test is still required, or if it can be skipped since it primarily increases coverage without validating specific behavior.

@webbnh
Copy link
Collaborator

webbnh commented Mar 5, 2026

it primarily increases coverage without validating specific behavior.

It is validating the absence of specific behavior, which is, in effect, validating specific behavior: it is showing that, if you call the function with a bad configuration, it still returns normally.

And, there is nothing wrong with increasing coverage when the cost to do so is this low.

Just checking if adding this test is still required, or if it can be skipped

It's not required -- this PR has been approved. Alternatively, as I said in Slack, you could just remove the uncovered code, instead of adding a test for it: if we don't mind crashing in that case, or if we're unwilling to test it, we would probably be better off if that check weren't there.

If we add a test for this, it would look something like

That looks like an excellent test. 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants