Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions tests/webapp/api/test_bugzilla.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,78 @@ def request_callback(request):
assert resp.json()["detail"] == "Authentication credentials were not provided."


def test_post_comment(client, activate_responses, test_user):
"""
test successfully posting a comment to a Bugzilla bug
"""

def request_callback(request):
headers = {}
requestdata = json.loads(request.body)
requestheaders = request.headers
assert requestheaders["x-bugzilla-api-key"] == "12345helloworld"
assert requestdata["comment"] == "Performance improvement detected."
assert requestdata["comment_tags"] == ["perf-alert"]
resp_body = {"id": 101}
return (200, headers, json.dumps(resp_body))

responses.add_callback(
responses.POST,
"https://thisisnotbugzilla.org/rest/bug/323/comment",
callback=request_callback,
content_type="application/json",
)

client.force_authenticate(user=test_user)

resp = client.post(
reverse("bugzilla-post-comment"),
{"bug_id": 323, "comment": "Performance improvement detected."},
)
assert resp.status_code == 200
assert resp.json()["id"] == 101


def test_post_comment_missing_bug_id(client, activate_responses, test_user):
"""
test that post_comment returns 400 when bug_id is missing
"""
client.force_authenticate(user=test_user)

resp = client.post(
reverse("bugzilla-post-comment"),
{"comment": "Performance improvement detected."},
)
assert resp.status_code == 400
assert resp.json()["failure"] == "bug_id is required"


def test_post_comment_missing_comment(client, activate_responses, test_user):
"""
test that post_comment returns 400 when comment is missing
"""
client.force_authenticate(user=test_user)

resp = client.post(
reverse("bugzilla-post-comment"),
{"bug_id": 323},
)
assert resp.status_code == 400
assert resp.json()["failure"] == "comment is required"


def test_post_comment_unauthenticated(client, activate_responses):
"""
test that post_comment requires authentication
"""
resp = client.post(
reverse("bugzilla-post-comment"),
{"bug_id": 323, "comment": "Performance improvement detected."},
)
assert resp.status_code == 403
assert resp.json()["detail"] == "Authentication credentials were not provided."


def test_create_bug_with_long_crash_signature(
client, eleven_jobs_stored, activate_responses, test_user
):
Expand Down
38 changes: 38 additions & 0 deletions treeherder/webapp/api/bugzilla.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,41 @@ def create_bug(self, request):
"url": get_bug_url(bug_id, settings.BUGFILER_API_URL),
}
)

@action(detail=False, methods=["post"])
def post_comment(self, request):
"""
Post a comment to an existing Bugzilla bug
"""
if settings.BUGFILER_API_KEY is None:
return Response({"failure": "Bugzilla API key not set!"}, status=HTTP_400_BAD_REQUEST)

params = request.data
bug_id = params.get("bug_id")
comment = params.get("comment")

if not bug_id:
return Response({"failure": "bug_id is required"}, status=HTTP_400_BAD_REQUEST)
if not comment:
return Response({"failure": "comment is required"}, status=HTTP_400_BAD_REQUEST)

url = f"{settings.BUGFILER_API_URL}/rest/bug/{bug_id}/comment"
headers = {
"x-bugzilla-api-key": settings.BUGFILER_API_KEY,
"Accept": "application/json",
}
data = {
"comment": comment,
"comment_tags": ["perf-alert"],
}

try:
response = make_request(url, method="POST", headers=headers, json=data)
except requests.exceptions.HTTPError as e:
try:
message = e.response.json()["message"]
except (ValueError, KeyError):
message = e.response.text
return Response({"failure": message}, status=HTTP_400_BAD_REQUEST)

return Response({"id": response.json().get("id")})
10 changes: 8 additions & 2 deletions ui/perfherder/alerts/FileBugModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export default class FileBugModal extends React.Component {
submitButtonText,
user,
errorMessage,
requireInput,
} = this.props;

const { inputValue, invalidInput, validated, disableButton } = this.state;
Expand All @@ -80,7 +81,7 @@ export default class FileBugModal extends React.Component {
<Row className="justify-content-left">
<Col className="col-6">
<Form.Label htmlFor="culpritBugId">
{title} <i>(optional): </i>
{title}{requireInput ? ':' : <i> (optional): </i>}
<span className="text-secondary">
<FontAwesomeIcon icon={faInfoCircle} title={infoText} />
</span>
Expand Down Expand Up @@ -114,7 +115,7 @@ export default class FileBugModal extends React.Component {
<Button
className="btn-outline-darker-info active"
onClick={(event) => this.handleSubmit(event, inputValue)}
disabled={(invalidInput && !validated) || disableButton}
disabled={(invalidInput && !validated) || (requireInput && !inputValue.length) || disableButton}
type="submit"
>
{(inputValue.length &&
Expand Down Expand Up @@ -146,4 +147,9 @@ FileBugModal.propTypes = {
title: PropTypes.string.isRequired,
submitButtonText: PropTypes.string.isRequired,
user: PropTypes.shape({}).isRequired,
requireInput: PropTypes.bool,
};

FileBugModal.defaultProps = {
requireInput: false,
};
104 changes: 104 additions & 0 deletions ui/perfherder/alerts/StatusDropdown.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default class StatusDropdown extends React.Component {
showBugModal: false,
showFileBugModal: false,
showCriticalFileBugModal: false,
showAddCommentModal: false,
showNotesModal: false,
showTagsModal: false,
selectedValue: issueTrackers[0]?.text,
Expand All @@ -46,6 +47,7 @@ export default class StatusDropdown extends React.Component {
),
isWeekend: isWeekend(),
fileBugErrorMessage: null,
addCommentErrorMessage: null,
};
}

Expand Down Expand Up @@ -317,6 +319,84 @@ export default class StatusDropdown extends React.Component {
}
};

addComment = async (bugId) => {
const {
alertSummary,
repoModel,
updateViewState,
filteredAlerts = [],
frameworks,
user,
} = this.props;
const { browsertimeAlertsExtraData } = this.state;

const perfCompareURL = getPerfCompareBaseURL(
alertSummary.repository,
alertSummary.prev_push_revision,
alertSummary.repository,
alertSummary.revision,
alertSummary.framework,
);

const result = await this.getBugTemplate(
alertSummary.framework,
updateViewState,
);
if (!result) {
return { failureStatus: 'Failed to retrieve bug template' };
}

const textualSummary = new TextualSummary(
frameworks,
filteredAlerts,
alertSummary,
null,
await browsertimeAlertsExtraData.enrichAndRetrieveAlerts(),
);
const templateArgs = this.getTemplateArgs(
frameworks,
alertSummary,
repoModel,
textualSummary,
user,
perfCompareURL,
);

templateSettings.interpolate = /{{([\s\S]+?)}}/g;
const fillTemplate = template(result.no_action_required_text);
const commentText = fillTemplate(templateArgs);
console.log(`commentText: ${commentText}`);

const createResult = await create(getApiUrl('/bugzilla/post_comment/'), {
bug_id: bugId,
comment: commentText,
});
if (createResult.failureStatus) {
return {
failureStatus: createResult.failureStatus,
data: createResult.data,
};
}
window.open(`${bzBaseUrl}show_bug.cgi?id=${bugId}`);
this.changeAlertSummary({ bug_number: bugId });

return { failureStatus: null };
};

addCommentAndClose = async (event, params, state) => {
event.preventDefault();
const bugId = params.bug_number;
const result = await this.addComment(bugId);

if (result.failureStatus) {
this.setState({
addCommentErrorMessage: `Failure: ${result.data}`,
});
} else {
this.toggle(state);
}
};

changeAlertSummary = async (params) => {
const { alertSummary, updateState, updateViewState } = this.props;

Expand Down Expand Up @@ -356,6 +436,7 @@ export default class StatusDropdown extends React.Component {
showBugModal,
showFileBugModal,
showCriticalFileBugModal,
showAddCommentModal,
showNotesModal,
showTagsModal,
selectedValue,
Expand Down Expand Up @@ -457,6 +538,23 @@ export default class StatusDropdown extends React.Component {
user={user}
errorMessage={this.state.fileBugErrorMessage}
/>
<FileBugModal
showModal={showAddCommentModal}
toggle={() => this.toggle('showAddCommentModal')}
updateAndClose={(event, inputValue) =>
this.addCommentAndClose(
event,
{ bug_number: parseInt(inputValue, 10) },
'showAddCommentModal',
)
}
header="Add Comment to Bug"
title="Enter Bug Number"
submitButtonText="Add Comment"
requireInput
user={user}
errorMessage={this.state.addCommentErrorMessage}
/>
<NotesModal
showModal={showNotesModal}
toggle={() => this.toggle('showNotesModal')}
Expand Down Expand Up @@ -524,6 +622,12 @@ export default class StatusDropdown extends React.Component {
Unlink from bug
</Dropdown.Item>
)}
<Dropdown.Item
as="a"
onClick={() => this.toggle('showAddCommentModal')}
>
Add comment to bug
</Dropdown.Item>
<Dropdown.Item
as="a"
onClick={() => this.toggle('showNotesModal')}
Expand Down