blob: 6718e1c09baede5640a486fe428c31d498c93aae [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import json
from typing import Any
from unittest.mock import MagicMock, patch
import pytest
from magpie_sourcehut.builds import get_job
from magpie_sourcehut.cli import main
from magpie_sourcehut.client import SourceHutError, query_graphql
from magpie_sourcehut.lists import get_patchset, list_patchsets, map_patchset_to_pr
from magpie_sourcehut.repo import get_repo
from magpie_sourcehut.todo import (
get_ticket,
label_ticket,
submit_comment,
submit_ticket,
update_ticket_status,
)
@pytest.fixture
def mock_env(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("SRHT_TOKEN", "mock_token_123")
def make_mock_response(status_code: int, body_dict: dict[str, Any]) -> MagicMock:
mock_resp = MagicMock()
mock_resp.read.return_value = json.dumps(body_dict).encode("utf-8")
mock_resp.status = status_code
return mock_resp
@patch("urllib.request.urlopen")
def test_query_graphql_success(mock_urlopen: MagicMock, mock_env: None) -> None:
mock_urlopen.return_value.__enter__.return_value = make_mock_response(200, {"data": {"version": "1.0"}})
res = query_graphql("todo", "{ version }")
assert res == {"version": "1.0"}
def test_query_graphql_no_token() -> None:
with pytest.raises(SourceHutError, match="SRHT_TOKEN environment variable is not set"):
query_graphql("todo", "{ version }")
@patch("urllib.request.urlopen")
def test_query_graphql_error_in_json(mock_urlopen: MagicMock, mock_env: None) -> None:
mock_urlopen.return_value.__enter__.return_value = make_mock_response(
200, {"errors": [{"message": "Invalid query syntax"}]}
)
with pytest.raises(SourceHutError, match=r"GraphQL error from todo\.sr\.ht: Invalid query syntax"):
query_graphql("todo", "invalid_query")
@patch("urllib.request.urlopen")
def test_get_ticket(mock_urlopen: MagicMock, mock_env: None) -> None:
ticket_data = {
"id": 42,
"title": "Fix memory leak",
"description": "Found a leak in client",
"status": "UNRESOLVED",
"resolution": None,
"labels": [{"id": 1, "name": "bug"}],
"comments": [],
}
mock_urlopen.return_value.__enter__.return_value = make_mock_response(
200, {"data": {"tracker": {"ticket": ticket_data}}}
)
res = get_ticket("~user", "my-project", 42)
assert res["id"] == 42
assert res["title"] == "Fix memory leak"
@patch("urllib.request.urlopen")
def test_submit_ticket(mock_urlopen: MagicMock, mock_env: None) -> None:
mock_urlopen.return_value.__enter__.return_value = make_mock_response(
200, {"data": {"submitTicket": {"id": 101, "title": "New issue"}}}
)
res = submit_ticket("~user", "my-project", "New issue", "Description here")
assert res["id"] == 101
@patch("urllib.request.urlopen")
def test_submit_comment(mock_urlopen: MagicMock, mock_env: None) -> None:
mock_urlopen.return_value.__enter__.return_value = make_mock_response(
200, {"data": {"submitComment": {"id": 501, "body": "Comment body"}}}
)
res = submit_comment("~user", "my-project", 42, "Comment body")
assert res["id"] == 501
@patch("urllib.request.urlopen")
def test_label_ticket(mock_urlopen: MagicMock, mock_env: None) -> None:
mock_urlopen.return_value.__enter__.return_value = make_mock_response(
200, {"data": {"labelTicket": {"id": 42}}}
)
res = label_ticket("~user", "my-project", 42, 10)
assert res == {"id": 42}
@patch("urllib.request.urlopen")
def test_update_ticket_status(mock_urlopen: MagicMock, mock_env: None) -> None:
mock_urlopen.return_value.__enter__.return_value = make_mock_response(
200, {"data": {"updateTicketStatus": {"id": 42, "status": "RESOLVED", "resolution": "FIXED"}}}
)
res = update_ticket_status("~user", "my-project", 42, "RESOLVED", "FIXED")
assert res["status"] == "RESOLVED"
@patch("urllib.request.urlopen")
def test_get_patchset_and_mapping(mock_urlopen: MagicMock, mock_env: None) -> None:
patchset_data = {
"id": 200,
"subject": "[PATCH 0/2] Fix some logs",
"version": 1,
"status": "PROPOSED",
"patches": [
{"id": 201, "subject": "[PATCH 1/2] Add warning log", "diff": "--- a/file\n+++ b/file\n"},
{"id": 202, "subject": "[PATCH 2/2] Add error log", "diff": "--- a/file2\n+++ b/file2\n"},
],
"thread": {
"id": 999,
"emails": {
"edges": [
{
"node": {
"id": 1000,
"subject": "[PATCH 0/2] Fix some logs",
"body": "Here is the patch series to fix logging",
"sender": {"canonicalName": "Alice <alice@example.com>"},
"date": "2026-07-01T12:00:00Z",
}
},
{
"node": {
"id": 1003,
"subject": "Re: [PATCH 0/2] Fix some logs",
"body": "Looks good to me!",
"sender": {"canonicalName": "Bob <bob@example.com>"},
"date": "2026-07-01T13:00:00Z",
}
},
]
},
},
}
mock_urlopen.return_value.__enter__.return_value = make_mock_response(
200, {"data": {"list": {"patchset": patchset_data}}}
)
raw = get_patchset("~user", "my-list", 200)
assert raw["id"] == 200
pr = map_patchset_to_pr(raw)
assert pr["id"] == "200"
assert pr["title"] == "[PATCH 0/2] Fix some logs"
assert pr["author"] == "Alice <alice@example.com>"
assert pr["description"] == "Here is the patch series to fix logging"
assert pr["state"] == "OPEN"
assert len(pr["commits"]) == 2
assert pr["commits"][0]["subject"] == "[PATCH 1/2] Add warning log"
assert len(pr["comments"]) == 1
assert pr["comments"][0]["author"] == "Bob <bob@example.com>"
assert pr["comments"][0]["body"] == "Looks good to me!"
@patch("urllib.request.urlopen")
def test_list_patchsets(mock_urlopen: MagicMock, mock_env: None) -> None:
mock_urlopen.return_value.__enter__.return_value = make_mock_response(
200,
{
"data": {
"list": {
"patches": {
"edges": [
{"node": {"id": 1, "subject": "P1", "status": "ACCEPTED"}},
{"node": {"id": 2, "subject": "P2", "status": "PROPOSED"}},
]
}
}
},
},
)
res = list_patchsets("~user", "my-list")
assert len(res) == 2
assert res[0]["subject"] == "P1"
@patch("urllib.request.urlopen")
def test_get_job(mock_urlopen: MagicMock, mock_env: None) -> None:
mock_urlopen.return_value.__enter__.return_value = make_mock_response(
200, {"data": {"job": {"id": 55, "status": "SUCCESS", "tasks": []}}}
)
res = get_job(55)
assert res["status"] == "SUCCESS"
@patch("urllib.request.urlopen")
def test_get_repo(mock_urlopen: MagicMock, mock_env: None) -> None:
mock_urlopen.return_value.__enter__.return_value = make_mock_response(
200, {"data": {"repository": {"id": 9, "name": "my-repo", "description": "VCS"}}}
)
res = get_repo("git", "~user", "my-repo")
assert res["name"] == "my-repo"
@patch("urllib.request.urlopen")
def test_cli_dispatch(mock_urlopen: MagicMock, mock_env: None, capsys: pytest.CaptureFixture[str]) -> None:
mock_urlopen.return_value.__enter__.return_value = make_mock_response(
200, {"data": {"job": {"id": 12, "status": "FAILED"}}}
)
code = main(["build", "get", "12"])
assert code == 0
captured = capsys.readouterr()
assert "FAILED" in captured.out
def test_cli_label_error(capsys: pytest.CaptureFixture[str]) -> None:
with pytest.raises(SystemExit) as excinfo:
main(["ticket", "label", "~user", "my-project", "42"])
assert excinfo.value.code == 2
captured = capsys.readouterr()
assert "At least one of --add or --remove must be specified" in captured.err