Skip to content

EPMRPP-115034 || add Jira client commands and update dependencies - #90

Merged
grabsefx merged 2 commits into
developfrom
EPMRPP-115034
Jun 26, 2026
Merged

EPMRPP-115034 || add Jira client commands and update dependencies#90
grabsefx merged 2 commits into
developfrom
EPMRPP-115034

Conversation

@grabsefx

@grabsefx grabsefx commented Jun 26, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features
    • Added new Jira actions for testing the connection, fetching an issue, listing issue types, loading issue fields, and creating tickets.
    • Improved ticket handling to support attachments and more complete field data when submitting issues.
  • Bug Fixes
    • Fixed Jira issue conversion to reuse shared parsing settings, improving consistency across ticket operations.
    • Strengthened validation for missing Jira connection details and project settings, with clearer failures when required information is unavailable.

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@grabsefx, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 34 minutes and 57 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b777acb6-a00b-408a-931e-822dab25a216

📥 Commits

Reviewing files that changed from the base of the PR and between 6543ebb and ce6eb20.

📒 Files selected for processing (1)
  • src/main/java/com/epam/reportportal/extension/bugtracking/jira/command/TestConnectionCommand.java

Walkthrough

The PR converts the Jira bugtracking integration from direct strategy methods to extension commands. It adds commands for connection checks, issue lookup, issue metadata, and ticket creation, updates shared Jira client and ticket conversion helpers, and rewrites the command tests.

Changes

Jira command migration

Layer / File(s) Summary
Shared Jira client and ticket utils
src/main/java/com/epam/reportportal/extension/bugtracking/jira/JIRATicketUtils.java, src/main/java/com/epam/reportportal/extension/bugtracking/jira/client/JiraClientProvider.java, build.gradle
JIRATicketUtils now accepts injected ObjectMapper instances, JiraClientProvider builds JiraRestClient from IntegrationParams, and the Jira service-api dependency version changes.
Strategy command mapping
src/main/java/com/epam/reportportal/extension/bugtracking/jira/JiraStrategy.java
JiraStrategy now exposes the Jira command map and plugin params from the new commands and no longer implements the old BtsExtension methods.
Connection and issue lookup
src/main/java/com/epam/reportportal/extension/bugtracking/jira/command/TestConnectionCommand.java, src/main/java/com/epam/reportportal/extension/bugtracking/jira/command/GetIssueCommand.java, src/test/java/com/epam/reportportal/extension/bugtracking/jira/command/BaseCommandTest.java, src/test/java/com/epam/reportportal/extension/bugtracking/jira/command/TestConnectionCommandTest.java, src/test/java/com/epam/reportportal/extension/bugtracking/jira/command/GetIssueCommandTest.java
TestConnectionCommand and GetIssueCommand now validate command arguments, resolve Jira clients through JiraClientProvider, and the shared tests invoke them through PluginCommandRQ instead of JiraStrategy methods.
Issue metadata commands
src/main/java/com/epam/reportportal/extension/bugtracking/jira/command/GetIssueTypesCommand.java, src/main/java/com/epam/reportportal/extension/bugtracking/jira/command/GetIssueFieldsCommand.java, src/test/java/com/epam/reportportal/extension/bugtracking/jira/command/GetIssueTypesCommandTest.java, src/test/java/com/epam/reportportal/extension/bugtracking/jira/command/GetIssueFieldsCommandTest.java
GetIssueTypesCommand and GetIssueFieldsCommand now return Jira issue types and create-field metadata, and their tests call the commands directly.
Post ticket command
src/main/java/com/epam/reportportal/extension/bugtracking/jira/command/PostTicketCommand.java, src/test/java/com/epam/reportportal/extension/bugtracking/jira/command/PostTicketCommandTest.java
PostTicketCommand parses PostTicketRQ, creates Jira issues, uploads binary attachments, and maps the created issue back to Ticket; its test now invokes the command directly.

Sequence Diagram

sequenceDiagram
  participant PostTicketCommand
  participant JiraClientProvider
  participant JiraRestClient
  participant DataStoreService
  participant JIRATicketUtils

  PostTicketCommand->>JiraClientProvider: provide(IntegrationParams)
  JiraClientProvider-->>PostTicketCommand: JiraRestClient
  PostTicketCommand->>JiraRestClient: issuesApi().createIssue(...)
  PostTicketCommand->>DataStoreService: load(binary token)
  PostTicketCommand->>JiraRestClient: attachmentsApi().addAttachment(...)
  PostTicketCommand->>JiraRestClient: issuesApi().getIssue(...)
  PostTicketCommand->>JIRATicketUtils: toTicket(issue, jiraUrl, objectMapper)
  JIRATicketUtils-->>PostTicketCommand: Ticket
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

I hop through Jira paths with glee,
New commands bloom like clover for me.
I fetch, I file, I bounce, I grin—
A rabbit’s code parade begins! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: new Jira client commands plus dependency updates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch EPMRPP-115034

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/com/epam/reportportal/extension/bugtracking/jira/command/GetIssueCommand.java`:
- Around line 64-65: The JQL built in GetIssueCommand.searchForIssue currently
concatenates ticketId directly into the query, which allows operator injection.
Update the issue search query to quote the ticketId value and escape any
embedded double quotes before passing it to
client.issueSearchApi().searchForIssuesUsingJql, so the constructed JQL stays as
a single issue identifier. Use GetIssueCommand and the searchForIssuesUsingJql
call as the locations to fix.

In
`@src/main/java/com/epam/reportportal/extension/bugtracking/jira/command/GetIssueFieldsCommand.java`:
- Around line 93-96: The Jira issue field parsing in GetIssueFieldsCommand
should not blindly cast the external “values” payload from
issueCreateMetadata.getAdditionalProperties(). Validate that the “values” entry
exists and is a list before iterating, and handle missing or unexpected shapes
as an integration error instead of letting a NullPointerException or
ClassCastException escape. In the stream setup inside GetIssueFieldsCommand,
remove the redundant convertValue step and process the map objects directly once
the type has been verified, using the existing issueCreateMetadata and
objectMapper flow.

In
`@src/main/java/com/epam/reportportal/extension/bugtracking/jira/command/PostTicketCommand.java`:
- Around line 187-214: The attachment upload path in
PostTicketCommand.addAttachment is using the raw JiraProps.PASSWORD value
instead of the decrypted password, so Basic auth is built with the wrong
credential. Update the credential retrieval in this method to decrypt the
password the same way JiraClientProvider.provide(...) does before constructing
the Authorization header, and keep the rest of the upload flow unchanged.
- Around line 156-164: The issue in PostTicketCommand is that fetch failure is
hidden by returning null after the Jira search, which can cause later NPEs and
is inconsistent with GetIssueCommand. Update the logic in the issue lookup flow
so that when client.issuesApi().getIssue(...) cannot retrieve the created issue
after search results indicate it exists, it throws a ReportPortalException with
the integration error instead of returning null. Keep the change localized to
the search/get path around JIRATicketUtils.toTicket and the jiraUrl resolution.
- Around line 109-126: The issue is that PostTicketCommand dereferences
issueType before confirming it was actually found in fields, so an empty list or
missing issuetype can fail with an uncontrolled exception. Update the validation
in PostTicketCommand to first ensure the issuetype field exists and has a value
before calling getValue() or get(0), and return the existing
UNABLE_INTERACT_WITH_INTEGRATION error through the same verification flow if it
is missing or empty. Use the issueType and fields lookup logic in the
PostTicketCommand loop as the place to add this guard.
- Around line 215-216: The manual attachment upload in PostTicketCommand uses a
default HttpClient without explicit timeout settings, so update the
HttpClients.createDefault() usage to a configured client built from
HttpClients.custom() with a RequestConfig applied. In the attachment upload path
inside PostTicketCommand, set connection and response/request timeouts on the
client before httpClient.execute(...) so slow Jira responses do not block the
command indefinitely.
- Around line 198-200: The attachment-loading logic in PostTicketCommand does
not close the InputStream returned by dataStoreService.load, so update the
data.get() handling to use try-with-resources around the loaded stream before
calling IOUtils.toByteArray; keep the existing Optional check, but ensure the
stream is automatically closed after reading the bytes.
- Around line 208-224: The attachment upload in PostTicketCommand is happening
inside the file iteration while reusing the same multipart builder, which causes
previously added files to be sent again on later passes. Move the HttpPost
creation and httpClient.execute upload logic out of the attachment collection
loop so all files are added first, then perform a single upload for the
issueKey. Keep the fix localized around the attachment-building flow in
PostTicketCommand and ensure the multipart entity is built only once after all
attachments are gathered.

In
`@src/test/java/com/epam/reportportal/extension/bugtracking/jira/command/PostTicketCommandTest.java`:
- Around line 97-100: The addAttachmentTest in PostTicketCommandTest is only a
placeholder and always passes, so either remove this empty test or replace it
with real coverage for the attachment flow through the relevant
postTicket/addAttachment path. If you keep it, make sure it asserts the
credential handling, upload loop, and stream handling behavior in the production
attachment logic instead of just containing a comment.
- Around line 92-94: In PostTicketCommandTest, the test is dereferencing the
returned Ticket before asserting it is non-null and it no longer validates the
mapped fields. Update the assertion order so the null check happens before using
ticket.getTicketUrl(), and restore the field-level assertions in the test for
the Ticket returned by command.invokeCommand(INTEGRATION, rq), including ticket
URL, ticket id, and status.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 017dab35-1a68-4949-9c10-e45f011a9454

📥 Commits

Reviewing files that changed from the base of the PR and between d21f102 and 6543ebb.

📒 Files selected for processing (15)
  • build.gradle
  • src/main/java/com/epam/reportportal/extension/bugtracking/jira/JIRATicketUtils.java
  • src/main/java/com/epam/reportportal/extension/bugtracking/jira/JiraStrategy.java
  • src/main/java/com/epam/reportportal/extension/bugtracking/jira/client/JiraClientProvider.java
  • src/main/java/com/epam/reportportal/extension/bugtracking/jira/command/GetIssueCommand.java
  • src/main/java/com/epam/reportportal/extension/bugtracking/jira/command/GetIssueFieldsCommand.java
  • src/main/java/com/epam/reportportal/extension/bugtracking/jira/command/GetIssueTypesCommand.java
  • src/main/java/com/epam/reportportal/extension/bugtracking/jira/command/PostTicketCommand.java
  • src/main/java/com/epam/reportportal/extension/bugtracking/jira/command/TestConnectionCommand.java
  • src/test/java/com/epam/reportportal/extension/bugtracking/jira/command/BaseCommandTest.java
  • src/test/java/com/epam/reportportal/extension/bugtracking/jira/command/GetIssueCommandTest.java
  • src/test/java/com/epam/reportportal/extension/bugtracking/jira/command/GetIssueFieldsCommandTest.java
  • src/test/java/com/epam/reportportal/extension/bugtracking/jira/command/GetIssueTypesCommandTest.java
  • src/test/java/com/epam/reportportal/extension/bugtracking/jira/command/PostTicketCommandTest.java
  • src/test/java/com/epam/reportportal/extension/bugtracking/jira/command/TestConnectionCommandTest.java

Comment on lines +64 to +65
SearchResults results = client.issueSearchApi()
.searchForIssuesUsingJql("issue = " + ticketId, null, 50, "", null, null, null, false, false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Jira REST API searchForIssuesUsingJql JQL escaping special characters values

💡 Result:

When working with the Jira REST API, there are two distinct layers where escaping is required: the transport layer (URL encoding for GET requests) and the query syntax layer (JQL-specific escaping). 1. REST API Transport (URL Encoding) When using GET requests to the /rest/api/3/search/jql endpoint, the entire JQL string must be URL-encoded [1]. If your JQL string contains special characters like spaces, quotes, or ampersands, they must be converted to their percent-encoded equivalents (e.g., a space becomes %20, & becomes %26) [1]. To avoid URL encoding complexities and length limitations, it is recommended to use POST requests instead [1][2]. In a POST request, the JQL is passed as a JSON object field, which eliminates the need to URL-encode the JQL string itself [1]. 2. JQL Syntax Escaping Within the JQL string itself, specific characters require handling to be interpreted correctly: - Exact Phrase Matching: To search for an exact phrase containing special characters or spaces, enclose the value in double quotes and nest additional escaped double quotes around the phrase [3][4]. For example, to search for "full screen" in a summary, use: summary ~ ""full screen"" [3][4]. - Reserved Characters: Jira's text-search index treats characters like +, -, &, |,!, (,), {, }, [, ], ^, ~, *,?, , and: as reserved [5][6]. These characters are often ignored or treated as operators by the search index; they cannot typically be "escaped" in a way that forces them to be stored or indexed as part of the text [5][6][7]. - Valid Escape Sequences: If you are constructing JQL strings programmatically, Jira recognizes standard escape sequences within strings, such as: ' (single quote), " (double quote), \t (tab), \n (newline), \r (carriage return), and \ (backslash) [8]. Summary of Best Practices: - Prefer POST over GET to avoid URL encoding issues [1]. - Use nested double quotes (e.g., ""value"") for phrase searches [3][4]. - Be aware that symbols like - or & are often stripped or treated as operators by the underlying Lucene index and cannot be escaped to force an exact match in free-text fields [5][6][9].

Citations:


Potential JQL injection: ticketId is not quoted

The ticketId value is concatenated directly into the JQL string without quoting. An input like ISSUE-123 OR project = "Public" executes unintended logic, bypassing the intended filter because JQL interprets unquoted tokens as operators or field names.

Wrap the value in double quotes and escape internal quotes (e.g., issue = \"" + ticketId.replace(""", "\"") + \""). Even standard issue keys should be quoted to handle special characters or spaces and prevent operator injection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/epam/reportportal/extension/bugtracking/jira/command/GetIssueCommand.java`
around lines 64 - 65, The JQL built in GetIssueCommand.searchForIssue currently
concatenates ticketId directly into the query, which allows operator injection.
Update the issue search query to quote the ticketId value and escape any
embedded double quotes before passing it to
client.issueSearchApi().searchForIssuesUsingJql, so the constructed JQL stays as
a single issue identifier. Use GetIssueCommand and the searchForIssuesUsingJql
call as the locations to fix.

Comment on lines +93 to +96
List<PostFormField> result = new ArrayList<>();
((List<Map<String, Object>>) issueCreateMetadata.getAdditionalProperties().get("values")).stream()
.map(a -> objectMapper.convertValue(a, new TypeReference<Map<String, Object>>() {}))
.map(field -> (JsonNode) objectMapper.valueToTree(field))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP 'class PageOfCreateMetaIssueTypeWithField' -g '*.java'
fd -e java PageOfCreateMetaIssueTypeWithField --exec cat -n {}

Repository: reportportal/plugin-bts-jira

Length of output: 166


🏁 Script executed:

rg -nP '..getCreateIssueMetaIssueTypeId..|getCreateIssueMeta' -g '*GetIssueFieldsCommand.java' --type java

Repository: reportportal/plugin-bts-jira

Length of output: 166


🏁 Script executed:

rg -P 'class .*AdditionalProperties|class .*CreateMeta' -g '*.java' --type java

Repository: reportportal/plugin-bts-jira

Length of output: 166


🏁 Script executed:

fd -e java "GetIssueFieldsCommand" --exec cat -n {}

Repository: reportportal/plugin-bts-jira

Length of output: 12104


Missing null/shape check before unchecked cast on external API data

The code at line 94 performs an unchecked cast ((List<Map<String, Object>>) ...) directly on the result of get("values") from issueCreateMetadata. If the Jira API response omits the "values" key or returns a different type, this throws a NullPointerException or ClassCastException at runtime, causing an opaque 500 error instead of a handled integration failure.

Additionally, the convertValue call on line 95 is redundant since the cast already assumes the element is a Map<String, Object>, which valueToTree can process directly.

Add a safe guard to validate the presence and type of the "values" entry before processing:

Proposed fix
-    List<PostFormField> result = new ArrayList<>();
-    ((List<Map<String, Object>>) issueCreateMetadata.getAdditionalProperties().get("values")).stream()
-        .map(a -> objectMapper.convertValue(a, new TypeReference<Map<String, Object>>() {}))
-        .map(field -> (JsonNode) objectMapper.valueToTree(field))
+    List<PostFormField> result = new ArrayList<>();
+    Object values = issueCreateMetadata.getAdditionalProperties().get("values");
+    if (!(values instanceof List<?> valueList)) {
+      throw new ReportPortalException(UNABLE_INTERACT_WITH_INTEGRATION,
+          "Create issue metadata does not contain field values for issue type '" + ticketType + "'");
+    }
+    valueList.stream()
+        .map(field -> (JsonNode) objectMapper.valueToTree(field))
         .forEach(jsonField -> {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
List<PostFormField> result = new ArrayList<>();
((List<Map<String, Object>>) issueCreateMetadata.getAdditionalProperties().get("values")).stream()
.map(a -> objectMapper.convertValue(a, new TypeReference<Map<String, Object>>() {}))
.map(field -> (JsonNode) objectMapper.valueToTree(field))
List<PostFormField> result = new ArrayList<>();
Object values = issueCreateMetadata.getAdditionalProperties().get("values");
if (!(values instanceof List<?> valueList)) {
throw new ReportPortalException(UNABLE_INTERACT_WITH_INTEGRATION,
"Create issue metadata does not contain field values for issue type '" + ticketType + "'");
}
valueList.stream()
.map(field -> (JsonNode) objectMapper.valueToTree(field))
.forEach(jsonField -> {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/epam/reportportal/extension/bugtracking/jira/command/GetIssueFieldsCommand.java`
around lines 93 - 96, The Jira issue field parsing in GetIssueFieldsCommand
should not blindly cast the external “values” payload from
issueCreateMetadata.getAdditionalProperties(). Validate that the “values” entry
exists and is a list before iterating, and handle missing or unexpected shapes
as an integration error instead of letting a NullPointerException or
ClassCastException escape. In the stream setup inside GetIssueFieldsCommand,
remove the redundant convertValue step and process the map objects directly once
the type has been verified, using the existing issueCreateMetadata and
objectMapper flow.

Comment on lines +109 to +126
PostFormField issueType = new PostFormField();
PostFormField components = new PostFormField();
for (PostFormField field : fields) {
if ("issuetype".equalsIgnoreCase(field.getId())) {
issueType = field;
}
if ("components".equalsIgnoreCase(field.getId())) {
components = field;
}
}

expect(issueType.getValue().size(),
com.epam.reportportal.base.infrastructure.persistence.commons.Predicates.equalTo(1))
.verify(UNABLE_INTERACT_WITH_INTEGRATION,
Suppliers.formattedSupplier("[IssueType] field has multiple values '{}' but should be only one",
issueType.getValue()));

final String issueTypeStr = issueType.getValue().get(0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Validate the required issue type before dereferencing it.

If fields is empty or does not contain issuetype, issueType.getValue().size() can throw before returning a controlled integration error.

Suggested fix
-    PostFormField issueType = new PostFormField();
+    PostFormField issueType = null;
     PostFormField components = new PostFormField();
     for (PostFormField field : fields) {
       if ("issuetype".equalsIgnoreCase(field.getId())) {
         issueType = field;
       }
@@
-    expect(issueType.getValue().size(),
+    expect(issueType, not(isNull()))
+        .verify(UNABLE_INTERACT_WITH_INTEGRATION, "[IssueType] field is required");
+    expect(issueType.getValue(), not(isNull()))
+        .verify(UNABLE_INTERACT_WITH_INTEGRATION, "[IssueType] field is required");
+    expect(issueType.getValue().size(),
         com.epam.reportportal.base.infrastructure.persistence.commons.Predicates.equalTo(1))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
PostFormField issueType = new PostFormField();
PostFormField components = new PostFormField();
for (PostFormField field : fields) {
if ("issuetype".equalsIgnoreCase(field.getId())) {
issueType = field;
}
if ("components".equalsIgnoreCase(field.getId())) {
components = field;
}
}
expect(issueType.getValue().size(),
com.epam.reportportal.base.infrastructure.persistence.commons.Predicates.equalTo(1))
.verify(UNABLE_INTERACT_WITH_INTEGRATION,
Suppliers.formattedSupplier("[IssueType] field has multiple values '{}' but should be only one",
issueType.getValue()));
final String issueTypeStr = issueType.getValue().get(0);
PostFormField issueType = null;
PostFormField components = new PostFormField();
for (PostFormField field : fields) {
if ("issuetype".equalsIgnoreCase(field.getId())) {
issueType = field;
}
if ("components".equalsIgnoreCase(field.getId())) {
components = field;
}
}
expect(issueType, not(isNull()))
.verify(UNABLE_INTERACT_WITH_INTEGRATION, "[IssueType] field is required");
expect(issueType.getValue(), not(isNull()))
.verify(UNABLE_INTERACT_WITH_INTEGRATION, "[IssueType] field is required");
expect(issueType.getValue().size(),
com.epam.reportportal.base.infrastructure.persistence.commons.Predicates.equalTo(1))
.verify(UNABLE_INTERACT_WITH_INTEGRATION,
Suppliers.formattedSupplier("[IssueType] field has multiple values '{}' but should be only one",
issueType.getValue()));
final String issueTypeStr = issueType.getValue().get(0);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/epam/reportportal/extension/bugtracking/jira/command/PostTicketCommand.java`
around lines 109 - 126, The issue is that PostTicketCommand dereferences
issueType before confirming it was actually found in fields, so an empty list or
missing issuetype can fail with an uncontrolled exception. Update the validation
in PostTicketCommand to first ensure the issuetype field exists and has a value
before calling getValue() or get(0), and return the existing
UNABLE_INTERACT_WITH_INTEGRATION error through the same verification flow if it
is missing or empty. Use the issueType and fields lookup logic in the
PostTicketCommand loop as the place to add this guard.

Comment on lines +156 to +164
SearchResults results = client.issueSearchApi()
.searchForIssuesUsingJql("issue = " + issueKey, null, 50, "", null, null, null, false, false);
if (results.getTotal() > 0) {
IssueBean issue = client.issuesApi().getIssue(issueKey, null, null, null, null, null, null);
String jiraUrl = JiraProps.URL.getParam(params)
.orElseThrow(() -> new ReportPortalException(UNABLE_INTERACT_WITH_INTEGRATION, "Url is not specified."));
return JIRATicketUtils.toTicket(issue, jiraUrl, objectMapper);
}
return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Throw when the created issue cannot be fetched.

Returning null makes downstream callers fail later with an NPE and differs from GetIssueCommand, which raises a clear integration error when Jira search returns no issue.

Suggested fix
     if (results.getTotal() > 0) {
       IssueBean issue = client.issuesApi().getIssue(issueKey, null, null, null, null, null, null);
       String jiraUrl = JiraProps.URL.getParam(params)
           .orElseThrow(() -> new ReportPortalException(UNABLE_INTERACT_WITH_INTEGRATION, "Url is not specified."));
       return JIRATicketUtils.toTicket(issue, jiraUrl, objectMapper);
     }
-    return null;
+    throw new ReportPortalException(UNABLE_INTERACT_WITH_INTEGRATION, "Ticket not found: " + issueKey);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
SearchResults results = client.issueSearchApi()
.searchForIssuesUsingJql("issue = " + issueKey, null, 50, "", null, null, null, false, false);
if (results.getTotal() > 0) {
IssueBean issue = client.issuesApi().getIssue(issueKey, null, null, null, null, null, null);
String jiraUrl = JiraProps.URL.getParam(params)
.orElseThrow(() -> new ReportPortalException(UNABLE_INTERACT_WITH_INTEGRATION, "Url is not specified."));
return JIRATicketUtils.toTicket(issue, jiraUrl, objectMapper);
}
return null;
SearchResults results = client.issueSearchApi()
.searchForIssuesUsingJql("issue = " + issueKey, null, 50, "", null, null, null, false, false);
if (results.getTotal() > 0) {
IssueBean issue = client.issuesApi().getIssue(issueKey, null, null, null, null, null, null);
String jiraUrl = JiraProps.URL.getParam(params)
.orElseThrow(() -> new ReportPortalException(UNABLE_INTERACT_WITH_INTEGRATION, "Url is not specified."));
return JIRATicketUtils.toTicket(issue, jiraUrl, objectMapper);
}
throw new ReportPortalException(UNABLE_INTERACT_WITH_INTEGRATION, "Ticket not found: " + issueKey);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/epam/reportportal/extension/bugtracking/jira/command/PostTicketCommand.java`
around lines 156 - 164, The issue in PostTicketCommand is that fetch failure is
hidden by returning null after the Jira search, which can cause later NPEs and
is inconsistent with GetIssueCommand. Update the logic in the issue lookup flow
so that when client.issuesApi().getIssue(...) cannot retrieve the created issue
after search results indicate it exists, it throws a ReportPortalException with
the integration error instead of returning null. Keep the change localized to
the search/get path around JIRATicketUtils.toTicket and the jiraUrl resolution.

Comment on lines +187 to +214
String username = JiraProps.USER_NAME.getParam(params)
.orElseThrow(() -> new ReportPortalException(UNABLE_INTERACT_WITH_INTEGRATION, "Username is not specified."));
String password = JiraProps.PASSWORD.getParam(params)
.orElseThrow(() -> new ReportPortalException(UNABLE_INTERACT_WITH_INTEGRATION, "Password is not specified."));

int count = 0;
MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create()
.setLaxMode()
.setCharset(StandardCharsets.UTF_8);

for (Map.Entry<String, String> entry : binaryData.entrySet()) {
Optional<InputStream> data = dataStoreService.load(entry.getKey());
if (data.isPresent()) {
byte[] bytes = IOUtils.toByteArray(data.get());
if (bytes.length == 0) {
LOGGER.warn("Empty file {}", entry.getValue());
continue;
}
entityBuilder.addPart("file", new ByteArrayBody(bytes, entry.getValue()));
count++;
}
if (count > 0) {
HttpPost request = new HttpPost(url + String.format("/rest/api/latest/issue/%s/attachments", issueKey));
request.setEntity(entityBuilder.build());
request.setHeader("X-Atlassian-Token", "no-check");
String plainCreds = username + ":" + password;
request.setHeader("Authorization",
"Basic " + new String(Base64.encodeBase64(plainCreds.getBytes(StandardCharsets.UTF_8))));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the decrypted Jira password for attachment uploads.

JiraClientProvider.provide(...) decrypts JiraProps.PASSWORD before authenticating, but addAttachment sends the raw stored value in Basic auth, so attachment uploads will fail for encrypted integration params.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/epam/reportportal/extension/bugtracking/jira/command/PostTicketCommand.java`
around lines 187 - 214, The attachment upload path in
PostTicketCommand.addAttachment is using the raw JiraProps.PASSWORD value
instead of the decrypted password, so Basic auth is built with the wrong
credential. Update the credential retrieval in this method to decrypt the
password the same way JiraClientProvider.provide(...) does before constructing
the Authorization header, and keep the rest of the upload flow unchanged.

Comment on lines +198 to +200
Optional<InputStream> data = dataStoreService.load(entry.getKey());
if (data.isPresent()) {
byte[] bytes = IOUtils.toByteArray(data.get());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Close loaded attachment streams.

IOUtils.toByteArray(data.get()) consumes the stream but does not close it; wrap the loaded stream in try-with-resources.

Suggested fix
       Optional<InputStream> data = dataStoreService.load(entry.getKey());
       if (data.isPresent()) {
-        byte[] bytes = IOUtils.toByteArray(data.get());
+        byte[] bytes;
+        try (InputStream inputStream = data.get()) {
+          bytes = IOUtils.toByteArray(inputStream);
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Optional<InputStream> data = dataStoreService.load(entry.getKey());
if (data.isPresent()) {
byte[] bytes = IOUtils.toByteArray(data.get());
Optional<InputStream> data = dataStoreService.load(entry.getKey());
if (data.isPresent()) {
byte[] bytes;
try (InputStream inputStream = data.get()) {
bytes = IOUtils.toByteArray(inputStream);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/epam/reportportal/extension/bugtracking/jira/command/PostTicketCommand.java`
around lines 198 - 200, The attachment-loading logic in PostTicketCommand does
not close the InputStream returned by dataStoreService.load, so update the
data.get() handling to use try-with-resources around the loaded stream before
calling IOUtils.toByteArray; keep the existing Optional check, but ensure the
stream is automatically closed after reading the bytes.

Comment on lines +208 to +224
if (count > 0) {
HttpPost request = new HttpPost(url + String.format("/rest/api/latest/issue/%s/attachments", issueKey));
request.setEntity(entityBuilder.build());
request.setHeader("X-Atlassian-Token", "no-check");
String plainCreds = username + ":" + password;
request.setHeader("Authorization",
"Basic " + new String(Base64.encodeBase64(plainCreds.getBytes(StandardCharsets.UTF_8))));
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
httpClient.execute(request, response -> {
if (!(response.getCode() >= 200 && response.getCode() < 300)) {
LOGGER.error("{} {}", response.getCode(), response.getReasonPhrase());
throw new ReportPortalException("Failed to upload attachment for " + issueKey);
}
return response;
});
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Move the upload request outside the attachment loop.

The current placement uploads after each found file while reusing the same multipart builder, so later iterations can re-upload earlier attachments.

Suggested fix
-      if (count > 0) {
-        HttpPost request = new HttpPost(url + String.format("/rest/api/latest/issue/%s/attachments", issueKey));
-        request.setEntity(entityBuilder.build());
-        request.setHeader("X-Atlassian-Token", "no-check");
-        String plainCreds = username + ":" + password;
-        request.setHeader("Authorization",
-            "Basic " + new String(Base64.encodeBase64(plainCreds.getBytes(StandardCharsets.UTF_8))));
-        try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
-          httpClient.execute(request, response -> {
-            if (!(response.getCode() >= 200 && response.getCode() < 300)) {
-              LOGGER.error("{} {}", response.getCode(), response.getReasonPhrase());
-              throw new ReportPortalException("Failed to upload attachment for " + issueKey);
-            }
-            return response;
-          });
-        }
-      }
     }
+    if (count > 0) {
+      HttpPost request = new HttpPost(url + String.format("/rest/api/latest/issue/%s/attachments", issueKey));
+      request.setEntity(entityBuilder.build());
+      request.setHeader("X-Atlassian-Token", "no-check");
+      String plainCreds = username + ":" + password;
+      request.setHeader("Authorization",
+          "Basic " + new String(Base64.encodeBase64(plainCreds.getBytes(StandardCharsets.UTF_8))));
+      try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
+        httpClient.execute(request, response -> {
+          if (!(response.getCode() >= 200 && response.getCode() < 300)) {
+            LOGGER.error("{} {}", response.getCode(), response.getReasonPhrase());
+            throw new ReportPortalException("Failed to upload attachment for " + issueKey);
+          }
+          return response;
+        });
+      }
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/epam/reportportal/extension/bugtracking/jira/command/PostTicketCommand.java`
around lines 208 - 224, The attachment upload in PostTicketCommand is happening
inside the file iteration while reusing the same multipart builder, which causes
previously added files to be sent again on later passes. Move the HttpPost
creation and httpClient.execute upload logic out of the attachment collection
loop so all files are added first, then perform a single upload for the
issueKey. Keep the fix localized around the attachment-building flow in
PostTicketCommand and ensure the multipart entity is built only once after all
attachments are gathered.

Comment on lines +215 to +216
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
httpClient.execute(request, response -> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/main/java/com/epam/reportportal/extension/bugtracking/jira/command/PostTicketCommand.java

Repository: reportportal/plugin-bts-jira

Length of output: 13474


🏁 Script executed:

find src -name "*.java" -type f -exec grep -l "CloseableHttpClient\|HttpClients.create" {} \;

Repository: reportportal/plugin-bts-jira

Length of output: 260


🏁 Script executed:

grep -r "RequestConfig\|SocketTimeout\|ConnectTimeout" src/main/java --include="*.java" | head -20

Repository: reportportal/plugin-bts-jira

Length of output: 166


🏁 Script executed:

python3 - <<'PY'
import subprocess
import re

# Search for HTTP client configuration patterns
result = subprocess.run(
    ['rg', 'RequestConfig|setTimeout|timeout', '--type=java', '-A', '3', '-B', '2', 'src/'],
    capture_output=True,
    text=True
)

print("HTTP Client Configuration Patterns:")
print(result.stdout[:3000] if result.stdout else "No matches found")
if result.stderr:
    print("\nErrors:")
    print(result.stderr[:1000])
PY

Repository: reportportal/plugin-bts-jira

Length of output: 219


Configure timeouts for the manual attachment HTTP client.

The HttpClients.createDefault() call on line 215 creates an HttpClient with default configuration that lacks explicit request, connection, and socket timeouts. This can cause the command thread to block indefinitely if the Jira server is slow or unresponsive during attachment upload.

Configure timeouts using RequestConfig:

RequestConfig requestConfig = RequestConfig.custom()
    .setConnectTimeout(Duration.ofSeconds(10))
    .setResponseTimeout(Duration.ofSeconds(30))
    .build();

CloseableHttpClient httpClient = HttpClients.custom()
    .setDefaultRequestConfig(requestConfig)
    .build();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/epam/reportportal/extension/bugtracking/jira/command/PostTicketCommand.java`
around lines 215 - 216, The manual attachment upload in PostTicketCommand uses a
default HttpClient without explicit timeout settings, so update the
HttpClients.createDefault() usage to a configured client built from
HttpClients.custom() with a RequestConfig applied. In the attachment upload path
inside PostTicketCommand, set connection and response/request timeouts on the
client before httpClient.execute(...) so slow Jira responses do not block the
command indefinitely.

Comment on lines +92 to 94
Ticket ticket = command.invokeCommand(INTEGRATION, rq);
log.info(ticket.getTicketUrl());

assertNotNull(ticket);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert before dereferencing and restore ticket-field checks.

ticket.getTicketUrl() runs before the null assertion, and the test no longer verifies the returned ticket URL/id/status, so mapping regressions can pass unnoticed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/epam/reportportal/extension/bugtracking/jira/command/PostTicketCommandTest.java`
around lines 92 - 94, In PostTicketCommandTest, the test is dereferencing the
returned Ticket before asserting it is non-null and it no longer validates the
mapped fields. Update the assertion order so the null check happens before using
ticket.getTicketUrl(), and restore the field-level assertions in the test for
the Ticket returned by command.invokeCommand(INTEGRATION, rq), including ticket
URL, ticket id, and status.

Comment on lines 97 to +100
@Test
@DisabledIf("disabled")
void addAttachmentTest() {
var validJiraTicket = "EPMRPP-100426";
Map<String, String> map = new HashMap<>();
map.put("file1", "file1.txt");
//map.put("file2", "file2.txt");
lenient().when(dataStoreService.load(anyString()))
.thenReturn(
Optional.ofNullable(getClass().getClassLoader().getResourceAsStream("attachment.txt")),
Optional.ofNullable(getClass().getClassLoader().getResourceAsStream("attachment2.txt"))
);
jiraStrategy.addAttachment(validJiraTicket, INTEGRATION, map);
// attachment testing is done via the full postTicket flow

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Do not keep an empty passing attachment test.

This test body always passes while the production attachment path contains credential, upload-loop, and stream-handling logic that should be covered or the placeholder should be removed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/epam/reportportal/extension/bugtracking/jira/command/PostTicketCommandTest.java`
around lines 97 - 100, The addAttachmentTest in PostTicketCommandTest is only a
placeholder and always passes, so either remove this empty test or replace it
with real coverage for the attachment flow through the relevant
postTicket/addAttachment path. If you keep it, make sure it asserts the
credential handling, upload loop, and stream handling behavior in the production
attachment logic instead of just containing a comment.

@grabsefx
grabsefx merged commit 1192ad8 into develop Jun 26, 2026
3 checks passed
@grabsefx
grabsefx deleted the EPMRPP-115034 branch June 26, 2026 09:05
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