From 8d4781d39541b108ffe3c3acf95052c5ea32d22f Mon Sep 17 00:00:00 2001 From: hemengyang Date: Tue, 4 Jan 2022 20:04:28 +0800 Subject: [PATCH] =?UTF-8?q?change:=20=E8=B0=83=E6=95=B4=E7=94=9F=E6=88=90?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E4=BF=A1=E6=81=AF=E7=9A=84=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants.py | 4 +- src/models.py | 229 ++++++++++++++++++------------ src/process.py | 12 +- tests/models/test_adapter.py | 137 ++++-------------- tests/models/test_bot.py | 75 ++++------ tests/models/test_plugin.py | 111 ++++++--------- tests/models/test_tags.py | 93 ++++++++++++ tests/process/test_issues.py | 2 +- tests/utils/test_comment_issue.py | 6 +- 9 files changed, 354 insertions(+), 315 deletions(-) create mode 100644 tests/models/test_tags.py diff --git a/src/constants.py b/src/constants.py index 842128cf..cabf7ac5 100644 --- a/src/constants.py +++ b/src/constants.py @@ -1,12 +1,12 @@ import re -COMMENT_TITLE = "# 📃 Publish Check Result" +COMMENT_TITLE = "# 📃 商店发布检查结果" COMMIT_MESSAGE_PREFIX = ":beers: publish" BRANCH_NAME_PREFIX = "publish/issue" -REUSE_MESSAGE = "♻️ This comment has been updated with the latest result." +REUSE_MESSAGE = "♻️ 评论已更新至最新检查结果" POWERED_BY_BOT_MESSAGE = "💪 Powered by NoneBot2 Publish Bot" diff --git a/src/models.py b/src/models.py index 1c34fe9e..31d7b313 100644 --- a/src/models.py +++ b/src/models.py @@ -5,11 +5,11 @@ from enum import Enum from functools import cache from pathlib import Path -from typing import Optional +from typing import Any, Optional, Union import requests from github.Issue import Issue -from pydantic import BaseModel, BaseSettings, SecretStr, validator +from pydantic import BaseModel, BaseSettings, SecretStr, ValidationError, validator from .constants import ( ADAPTER_DESC_PATTERN, @@ -29,6 +29,24 @@ ) +class MyValidationError(ValueError): + """验证错误错误""" + + def __init__( + self, + type: "PublishType", + raw_data: dict[str, Any], + errors: list[dict[str, Any]], + ) -> None: + self.type = type + self.raw_data = raw_data + self.errors = errors + + @property + def message(self) -> str: + return generate_validation_message(self) + + class PartialGithubEventHeadCommit(BaseModel): message: str @@ -107,7 +125,7 @@ def label_validator(cls, v: str) -> str: @validator("color", pre=True) def color_validator(cls, v: str) -> str: if not re.match(r"^#[0-9a-fA-F]{6}$", v): - raise ValueError("颜色不符合规则") + raise ValueError("标签颜色不符合十六进制颜色码规则") return v @@ -121,6 +139,15 @@ class PublishInfo(abc.ABC, BaseModel): tags: list[Tag] is_official: bool = False + @validator("homepage", pre=True) + def homepage_validator(cls, v: str) -> str: + status_code = check_url(v) + if status_code != 200: + raise ValueError( + f"""⚠️ 项目 主页 返回状态码 {status_code}。
请确保您的项目主页可访问。
""" + ) + return v + @validator("tags", pre=True) def tags_validator(cls, v: list[Tag]) -> list[Tag]: if len(v) > 3: @@ -146,27 +173,12 @@ def from_issue(self, issue: Issue) -> "PublishInfo": """从议题中获取所需信息""" raise NotImplementedError - @abc.abstractmethod - def get_type(self) -> PublishType: + @classmethod + @abc.abstractclassmethod + def get_type(cls) -> PublishType: """获取发布类型""" raise NotImplementedError - @property - @abc.abstractmethod - def is_valid(self) -> bool: - """检查是否满足要求""" - raise NotImplementedError - - @property - def homepage_status_code(self) -> Optional[int]: - """主页状态码""" - return check_url(self.homepage) - - @property - def is_homepage_valid(self) -> bool: - """主页是否可用""" - return self.homepage_status_code == 200 - @property def validation_message(self) -> str: """验证信息""" @@ -177,15 +189,20 @@ class PyPIMixin(BaseModel): module_name: str project_link: str - @property - def is_published(self) -> bool: - return check_pypi(self.project_link) + @validator("project_link", pre=True) + def project_link_validator(cls, v: str) -> str: + if not check_pypi(v): + raise ValueError( + f'⚠️ 包 {v} 未发布至 PyPI。
请将您的包发布至 PyPI。
' + ) + return v class BotPublishInfo(PublishInfo): """发布机器人所需信息""" - def get_type(self) -> PublishType: + @classmethod + def get_type(cls) -> PublishType: return PublishType.BOT def update_file(self, settings: Settings) -> None: @@ -204,23 +221,25 @@ def from_issue(cls, issue: Issue) -> "BotPublishInfo": if not (name and desc and author and homepage and tags): raise ValueError("无法获取机器人信息") - return BotPublishInfo( - name=name.group(1).strip(), - desc=desc.group(1).strip(), - author=author, - homepage=homepage.group(1).strip(), - tags=json.loads(tags.group(1).strip()), - ) + raw_data = { + "name": name.group(1).strip(), + "desc": desc.group(1).strip(), + "author": author, + "homepage": homepage.group(1).strip(), + "tags": json.loads(tags.group(1).strip()), + } - @property - def is_valid(self) -> bool: - return self.is_homepage_valid + try: + return BotPublishInfo(**raw_data) + except ValidationError as e: + raise MyValidationError(cls.get_type(), raw_data, e.errors()) class PluginPublishInfo(PublishInfo, PyPIMixin): """发布插件所需信息""" - def get_type(self) -> PublishType: + @classmethod + def get_type(cls) -> PublishType: return PublishType.PLUGIN def update_file(self, settings: Settings) -> None: @@ -249,25 +268,27 @@ def from_issue(cls, issue: Issue) -> "PluginPublishInfo": ): raise ValueError("无法获取插件信息") - return PluginPublishInfo( - module_name=module_name.group(1).strip(), - project_link=project_link.group(1).strip(), - name=name.group(1).strip(), - desc=desc.group(1).strip(), - author=author, - homepage=homepage.group(1).strip(), - tags=json.loads(tags.group(1).strip()), - ) + raw_data = { + "module_name": module_name.group(1).strip(), + "project_link": project_link.group(1).strip(), + "name": name.group(1).strip(), + "desc": desc.group(1).strip(), + "author": author, + "homepage": homepage.group(1).strip(), + "tags": json.loads(tags.group(1).strip()), + } - @property - def is_valid(self) -> bool: - return self.is_published and self.is_homepage_valid + try: + return PluginPublishInfo(**raw_data) + except ValidationError as e: + raise MyValidationError(cls.get_type(), raw_data, e.errors()) class AdapterPublishInfo(PublishInfo, PyPIMixin): """发布适配器所需信息""" - def get_type(self) -> PublishType: + @classmethod + def get_type(cls) -> PublishType: return PublishType.ADAPTER def update_file(self, settings: Settings) -> None: @@ -296,19 +317,20 @@ def from_issue(cls, issue: Issue) -> "AdapterPublishInfo": ): raise ValueError("无法获取适配器信息") - return AdapterPublishInfo( - module_name=module_name.group(1).strip(), - project_link=project_link.group(1).strip(), - name=name.group(1).strip(), - desc=desc.group(1).strip(), - author=author, - homepage=homepage.group(1).strip(), - tags=json.loads(tags.group(1).strip()), - ) + raw_data = { + "module_name": module_name.group(1).strip(), + "project_link": project_link.group(1).strip(), + "name": name.group(1).strip(), + "desc": desc.group(1).strip(), + "author": author, + "homepage": homepage.group(1).strip(), + "tags": json.loads(tags.group(1).strip()), + } - @property - def is_valid(self) -> bool: - return self.is_published and self.is_homepage_valid + try: + return AdapterPublishInfo(**raw_data) + except ValidationError as e: + raise MyValidationError(cls.get_type(), raw_data, e.errors()) def check_pypi(project_link: str) -> bool: @@ -332,44 +354,77 @@ def check_url(url: str) -> Optional[int]: pass -def generate_validation_message(info: PublishInfo) -> str: +def generate_validation_message(info: Union[PublishInfo, MyValidationError]) -> str: """生成验证信息""" - publish_info = f"{info.get_type().value}: {info.name}" - - if info.is_valid: - result = "✅ All tests passed, you are ready to go!" - else: - result = "⚠️ We have found following problem(s) in pre-publish progress:" + if isinstance(info, MyValidationError): + # 如果有错误 + publish_info: str = f"{info.type.value}: {info.raw_data['name']}" + result = "⚠️ 在发布检查过程中,我们发现以下问题:" + + errors: list[str] = [] + for error in info.errors: + if error["loc"][0] == "tags": + errors.append(f"
  • 第 {error['loc'][1]+1} 个{error['msg']}
  • ") + else: + errors.append(f"
  • {error['msg']}
  • ") - error_message = "" - errors: list[str] = [] - if info.homepage_status_code != 200: - errors.append( - f"""
  • ⚠️ Project homepage returns {info.homepage_status_code}.
    Please make sure that your project has a publicly visible homepage.
  • """ - ) - if isinstance(info, AdapterPublishInfo) or isinstance(info, PluginPublishInfo): - if not info.is_published: - errors.append( - f"""
  • ⚠️ Package {info.project_link} is not available on PyPI.
    Please publish your package to PyPI.
  • """ - ) - if len(errors) != 0: error_message = "".join(errors) error_message = f"
    {error_message}
    " + else: + # 一切正常时 + publish_info = f"{info.get_type().value}: {info.name}" + result = "✅ 所有测试通过,一切准备就绪!" + error_message = "" detail_message = "" details: list[str] = [] - if info.homepage_status_code == 200: + + # 验证失败的项 + error_keys = ( + [error["loc"][0] for error in info.errors] + if isinstance(info, MyValidationError) + else [] + ) + + # 标签 + tags = [] + if isinstance(info, PublishInfo): + tags = [f"{tag.label}-{tag.color}" for tag in info.tags] + elif "tags" not in error_keys: + tags = [f"{tag['label']}-{tag['color']}" for tag in info.raw_data["tags"]] + + if tags: + details.append(f"
  • ✅ 标签: {', '.join(tags)}
  • ") + + # 主页 + homepage = "" + if isinstance(info, PublishInfo): + homepage = info.homepage + elif "homepage" not in error_keys: + homepage = info.raw_data["homepage"] + if homepage: details.append( - f"""
  • ✅ Project homepage returns {info.homepage_status_code}.
  • """ + f"""
  • ✅ 项目 主页 返回状态码 {check_url(homepage)}.
  • """ ) + + # 发布情况 + project_link = "" if isinstance(info, AdapterPublishInfo) or isinstance(info, PluginPublishInfo): - if info.is_published: - details.append( - f"""
  • ✅ Package {info.project_link} is available on PyPI.
  • """ - ) + project_link = info.project_link + elif ( + isinstance(info, MyValidationError) + and info.type in [PublishType.PLUGIN, PublishType.ADAPTER] + and "project_link" not in error_keys + ): + project_link = info.raw_data["project_link"] + if project_link: + details.append( + f"""
  • ✅ 包 {project_link} 已发布至 PyPI
  • """ + ) + if len(details) != 0: detail_message = "".join(details) - detail_message = f"""
    Report Detail
    {detail_message}
    """ + detail_message = f"""
    测试详情
    {detail_message}
    """ return VALIDATION_MESSAGE_TEMPLATE.format( publish_info=publish_info, diff --git a/src/process.py b/src/process.py index 7956be11..488c09d3 100644 --- a/src/process.py +++ b/src/process.py @@ -4,6 +4,7 @@ from .constants import BRANCH_NAME_PREFIX from .models import ( + MyValidationError, PartialGitHubIssuesEvent, PartialGitHubPullRequestEvent, PartialGitHubPushEvent, @@ -98,14 +99,13 @@ def process_issues_event(settings: Settings, repo: Repository): logging.info("议题与发布无关,已跳过") return - info = extract_publish_info_from_issue(issue, publish_type) - # 自动给议题添加标签 issue.edit(labels=[publish_type.value]) # 检查是否满足发布要求 # 仅在通过检查的情况下创建拉取请求 - if info.is_valid: + try: + info = extract_publish_info_from_issue(issue, publish_type) # 创建新分支 # 命名示例 publish/issue123 branch_name = f"{BRANCH_NAME_PREFIX}{issue.number}" @@ -117,7 +117,9 @@ def process_issues_event(settings: Settings, repo: Repository): create_pull_request( repo, info, settings.input_config.base, branch_name, issue.number ) - else: + message = info.validation_message + except MyValidationError as e: + message = e.message logging.info("发布没通过检查,暂不创建拉取请求") - comment_issue(issue, info.validation_message) + comment_issue(issue, message) diff --git a/tests/models/test_adapter.py b/tests/models/test_adapter.py index f1d54e56..4583d821 100644 --- a/tests/models/test_adapter.py +++ b/tests/models/test_adapter.py @@ -7,7 +7,7 @@ from pydantic import ValidationError from pytest_mock import MockerFixture -from src.models import AdapterPublishInfo +from src.models import AdapterPublishInfo, MyValidationError def generate_issue_body( @@ -21,78 +21,22 @@ def generate_issue_body( return f"""**协议名称:**\n\n{name}\n\n**协议功能:**\n\n{desc}\n\n**PyPI 项目名:**\n\n{project_link}\n\n**协议 import 包名:**\n\n{module_name}\n\n**协议项目仓库/主页链接:**\n\n{homepage}\n\n**标签:**\n\n{json.dumps(tags)}""" -def test_adapter_info() -> None: - info = AdapterPublishInfo( - module_name="module_name", - project_link="project_link", - name="name", - desc="desc", - author="author", - homepage="https://www.baidu.com", - tags=[{"label": "test", "color": "#ffffff"}], - is_official=False, - ) - - assert OrderedDict(info.dict()) == OrderedDict( - module_name="module_name", - project_link="project_link", - name="name", - desc="desc", - author="author", - homepage="https://www.baidu.com", - tags=[{"label": "test", "color": "#ffffff"}], - is_official=False, - ) +def mocked_requests_get(url: str): + class MockResponse: + def __init__(self, status_code: int): + self.status_code = status_code + if url == "https://pypi.org/pypi/project_link/json": + return MockResponse(200) + if url == "https://v2.nonebot.dev": + return MockResponse(200) -def test_adapter_tags_invalid() -> None: - """测试标签不正确的情况""" - with pytest.raises(ValidationError) as e: - info = AdapterPublishInfo( - module_name="module_name", - project_link="project_link", - name="name", - desc="desc", - author="author", - homepage="https://www.baidu.com", - tags=[{"label": "test", "color": "#adbcdef"}], - is_official=False, - ) - assert "颜色不符合规则" in str(e.value) - - with pytest.raises(ValidationError) as e: - info = AdapterPublishInfo( - module_name="module_name", - project_link="project_link", - name="name", - desc="desc", - author="author", - homepage="https://www.baidu.com", - tags=[{"label": "12345678901", "color": "#adbcde"}], - is_official=False, - ) - assert "标签名称不能超过 10 个字符" in str(e.value) - - with pytest.raises(ValidationError) as e: - info = AdapterPublishInfo( - module_name="module_name", - project_link="project_link", - name="name", - desc="desc", - author="author", - homepage="https://www.baidu.com", - tags=[ - {"label": "1", "color": "#ffffff"}, - {"label": "2", "color": "#ffffff"}, - {"label": "3", "color": "#ffffff"}, - {"label": "4", "color": "#ffffff"}, - ], - is_official=False, - ) - assert "标签数量不能超过 3 个" in str(e.value) + return MockResponse(404) def test_adapter_from_issue(mocker: MockerFixture) -> None: + """测试从 issue 中构造 AdapterPublishInfo 的情况""" + mock_requests = mocker.patch("requests.get", side_effect=mocked_requests_get) mock_issue: Issue = mocker.MagicMock() mock_issue.body = generate_issue_body() mock_issue.user.login = "author" @@ -113,6 +57,7 @@ def test_adapter_from_issue(mocker: MockerFixture) -> None: def test_adapter_from_issue_trailing_whitespace(mocker: MockerFixture) -> None: """测试末尾如果有空格的情况""" + mock_requests = mocker.patch("requests.get", side_effect=mocked_requests_get) mock_issue: Issue = mocker.MagicMock() mock_issue.body = generate_issue_body( name="name ", @@ -137,19 +82,6 @@ def test_adapter_from_issue_trailing_whitespace(mocker: MockerFixture) -> None: ) -def mocked_requests_get(url: str): - class MockResponse: - def __init__(self, status_code: int): - self.status_code = status_code - - if url == "https://pypi.org/pypi/project_link/json": - return MockResponse(200) - if url == "https://v2.nonebot.dev": - return MockResponse(200) - - return MockResponse(404) - - def test_adapter_info_validation_success(mocker: MockerFixture) -> None: """测试验证成功的情况""" mock_requests = mocker.patch("requests.get", side_effect=mocked_requests_get) @@ -165,10 +97,9 @@ def test_adapter_info_validation_success(mocker: MockerFixture) -> None: is_official=False, ) - assert info.is_valid assert ( info.validation_message - == """> Adapter: name\n\n**✅ All tests passed, you are ready to go!**\n\n
    Report Detail
  • ✅ Project homepage returns 200.
  • ✅ Package project_link is available on PyPI.
  • """ + == """> Adapter: name\n\n**✅ 所有测试通过,一切准备就绪!**\n\n
    测试详情
  • ✅ 标签: test-#ffffff
  • ✅ 项目 主页 返回状态码 200.
  • ✅ 包 project_link 已发布至 PyPI
  • """ ) calls = [ @@ -181,22 +112,20 @@ def test_adapter_info_validation_success(mocker: MockerFixture) -> None: def test_adapter_info_validation_failed(mocker: MockerFixture) -> None: """测试验证失败的情况""" mock_requests = mocker.patch("requests.get", side_effect=mocked_requests_get) - - info = AdapterPublishInfo( - module_name="module_name", + mock_issue: Issue = mocker.MagicMock() + mock_issue.body = generate_issue_body( project_link="project_link_failed", - name="name", - desc="desc", - author="author", homepage="https://www.baidu.com", - tags=[{"label": "test", "color": "#ffffff"}], - is_official=False, + tags=[{"label": "test", "color": "#fffffff"}], ) + mock_issue.user.login = "author" + + with pytest.raises(MyValidationError) as e: + AdapterPublishInfo.from_issue(mock_issue) - assert not info.is_valid assert ( - info.validation_message - == """> Adapter: name\n\n**⚠️ We have found following problem(s) in pre-publish progress:**\n
  • ⚠️ Project homepage returns 404.
    Please make sure that your project has a publicly visible homepage.
  • ⚠️ Package project_link_failed is not available on PyPI.
    Please publish your package to PyPI.
  • """ + e.value.message + == """> Adapter: name\n\n**⚠️ 在发布检查过程中,我们发现以下问题:**\n
  • ⚠️ 包 project_link_failed 未发布至 PyPI。
    请将您的包发布至 PyPI。
  • ⚠️ 项目 主页 返回状态码 404。
    请确保您的项目主页可访问。
  • 第 1 个标签颜色不符合十六进制颜色码规则
  • """ ) calls = [ @@ -209,22 +138,18 @@ def test_adapter_info_validation_failed(mocker: MockerFixture) -> None: def test_adapter_info_validation_partial_failed(mocker: MockerFixture) -> None: """测试验证一部分失败的情况""" mock_requests = mocker.patch("requests.get", side_effect=mocked_requests_get) - - info = AdapterPublishInfo( - module_name="module_name", - project_link="project_link", - name="name", - desc="desc", - author="author", + mock_issue: Issue = mocker.MagicMock() + mock_issue.body = generate_issue_body( homepage="https://www.baidu.com", - tags=[{"label": "test", "color": "#ffffff"}], - is_official=False, ) + mock_issue.user.login = "author" + + with pytest.raises(MyValidationError) as e: + AdapterPublishInfo.from_issue(mock_issue) - assert not info.is_valid assert ( - info.validation_message - == """> Adapter: name\n\n**⚠️ We have found following problem(s) in pre-publish progress:**\n
  • ⚠️ Project homepage returns 404.
    Please make sure that your project has a publicly visible homepage.
  • \n
    Report Detail
  • ✅ Package project_link is available on PyPI.
  • """ + e.value.message + == """> Adapter: name\n\n**⚠️ 在发布检查过程中,我们发现以下问题:**\n
  • ⚠️ 项目 主页 返回状态码 404。
    请确保您的项目主页可访问。
  • \n
    测试详情
  • ✅ 标签: test-#ffffff
  • ✅ 包 project_link 已发布至 PyPI
  • """ ) calls = [ diff --git a/tests/models/test_bot.py b/tests/models/test_bot.py index 623221f4..7aff7e1e 100644 --- a/tests/models/test_bot.py +++ b/tests/models/test_bot.py @@ -5,7 +5,7 @@ from github.Issue import Issue from pytest_mock import MockerFixture -from src.models import BotPublishInfo +from src.models import BotPublishInfo, MyValidationError def generate_issue_body( @@ -17,28 +17,22 @@ def generate_issue_body( return f"""**机器人名称:**\n\n{name}\n\n**机器人功能:**\n\n{desc}\n\n**机器人项目仓库/主页链接:**\n\n{homepage}\n\n**标签:**\n\n{json.dumps(tags)}""" -def test_bot_info() -> None: - info = BotPublishInfo( - name="name", - desc="desc", - author="author", - homepage="https://www.baidu.com", - tags=[{"label": "test", "color": "#ffffff"}], - is_official=False, - ) +def mocked_requests_get(url: str): + class MockResponse: + def __init__(self, status_code: int): + self.status_code = status_code - assert OrderedDict(info.dict()) == OrderedDict( - name="name", - desc="desc", - author="author", - homepage="https://www.baidu.com", - tags=[{"label": "test", "color": "#ffffff"}], - is_official=False, - ) + if url == "https://pypi.org/pypi/project_link/json": + return MockResponse(200) + if url == "https://v2.nonebot.dev": + return MockResponse(200) + + return MockResponse(404) def test_bot_from_issue(mocker: MockerFixture) -> None: """测试从 issue 中构造 BotPublishInfo""" + mock_requests = mocker.patch("requests.get", side_effect=mocked_requests_get) mock_issue: Issue = mocker.MagicMock() mock_issue.body = generate_issue_body() mock_issue.user.login = "author" @@ -53,10 +47,12 @@ def test_bot_from_issue(mocker: MockerFixture) -> None: tags=[{"label": "test", "color": "#ffffff"}], is_official=False, ) + mock_requests.assert_called_once_with("https://v2.nonebot.dev") def test_bot_from_issue_trailing_whitespace(mocker: MockerFixture) -> None: """测试末尾如果有空格的情况""" + mock_requests = mocker.patch("requests.get", side_effect=mocked_requests_get) mock_issue: Issue = mocker.MagicMock() mock_issue.body = generate_issue_body( name="name ", @@ -75,19 +71,7 @@ def test_bot_from_issue_trailing_whitespace(mocker: MockerFixture) -> None: tags=[{"label": "test", "color": "#ffffff"}], is_official=False, ) - - -def mocked_requests_get(url: str): - class MockResponse: - def __init__(self, status_code: int): - self.status_code = status_code - - if url == "https://pypi.org/pypi/project_link/json": - return MockResponse(200) - if url == "https://v2.nonebot.dev": - return MockResponse(200) - - return MockResponse(404) + mock_requests.assert_called_once_with("https://v2.nonebot.dev") def test_bot_info_validation_success(mocker: MockerFixture) -> None: @@ -103,10 +87,9 @@ def test_bot_info_validation_success(mocker: MockerFixture) -> None: is_official=False, ) - assert info.is_valid assert ( info.validation_message - == """> Bot: name\n\n**✅ All tests passed, you are ready to go!**\n\n
    Report Detail
  • ✅ Project homepage returns 200.
  • """ + == """> Bot: name\n\n**✅ 所有测试通过,一切准备就绪!**\n\n
    测试详情
  • ✅ 标签: test-#ffffff
  • ✅ 项目 主页 返回状态码 200.
  • """ ) calls = [ @@ -118,21 +101,23 @@ def test_bot_info_validation_success(mocker: MockerFixture) -> None: def test_bot_info_validation_failed(mocker: MockerFixture) -> None: """测试验证失败的情况""" mock_requests = mocker.patch("requests.get", side_effect=mocked_requests_get) - - info = BotPublishInfo( - name="name", - desc="desc", - author="author", + mock_issue: Issue = mocker.MagicMock() + mock_issue.body = generate_issue_body( homepage="https://www.baidu.com", - tags=[{"label": "test", "color": "#ffffff"}], - is_official=False, + tags=[ + {"label": "test", "color": "#ffffff"}, + {"label": "testtoolong", "color": "#fffffff"}, + ], ) + mock_issue.user.login = "author" - assert not info.is_valid - assert ( - info.validation_message - == """> Bot: name\n\n**⚠️ We have found following problem(s) in pre-publish progress:**\n
  • ⚠️ Project homepage returns 404.
    Please make sure that your project has a publicly visible homepage.
  • """ - ) + try: + info = BotPublishInfo.from_issue(mock_issue) + except MyValidationError as e: + assert ( + e.message + == """> Bot: name\n\n**⚠️ 在发布检查过程中,我们发现以下问题:**\n
  • ⚠️ 项目 主页 返回状态码 404。
    请确保您的项目主页可访问。
  • 第 2 个标签名称不能超过 10 个字符
  • 第 2 个标签颜色不符合十六进制颜色码规则
  • """ + ) calls = [ mocker.call("https://www.baidu.com"), diff --git a/tests/models/test_plugin.py b/tests/models/test_plugin.py index a1dc2b10..48fcb065 100644 --- a/tests/models/test_plugin.py +++ b/tests/models/test_plugin.py @@ -2,10 +2,11 @@ import json from collections import OrderedDict +import pytest from github.Issue import Issue from pytest_mock import MockerFixture -from src.models import PluginPublishInfo +from src.models import MyValidationError, PluginPublishInfo def generate_issue_body( @@ -19,33 +20,24 @@ def generate_issue_body( return f"""**插件名称:**\n\n{name}\n\n**插件功能:**\n\n{desc}\n\n**PyPI 项目名:**\n\n{project_link}\n\n**插件 import 包名:**\n\n{module_name}\n\n**插件项目仓库/主页链接:**\n\n{homepage}\n\n**标签:**\n\n{json.dumps(tags)}""" -def test_plugin_info() -> None: - info = PluginPublishInfo( - module_name="module_name", - project_link="project_link", - name="name", - desc="desc", - author="author", - homepage="https://www.baidu.com", - tags=[{"label": "test", "color": "#ffffff"}], - is_official=False, - ) +def mocked_requests_get(url: str): + class MockResponse: + def __init__(self, status_code: int): + self.status_code = status_code - assert OrderedDict(info.dict()) == OrderedDict( - module_name="module_name", - project_link="project_link", - name="name", - desc="desc", - author="author", - homepage="https://www.baidu.com", - tags=[{"label": "test", "color": "#ffffff"}], - is_official=False, - ) + if url == "https://pypi.org/pypi/project_link/json": + return MockResponse(200) + if url == "https://v2.nonebot.dev": + return MockResponse(200) + + return MockResponse(404) def test_plugin_from_issue(mocker: MockerFixture) -> None: + """测试从 issue 中构造 PluginPublishInfo 的情况""" + mock_requests = mocker.patch("requests.get", side_effect=mocked_requests_get) mock_issue: Issue = mocker.MagicMock() - mock_issue.body = generate_issue_body(homepage="https://www.baidu.com") + mock_issue.body = generate_issue_body() mock_issue.user.login = "author" info = PluginPublishInfo.from_issue(mock_issue) @@ -56,21 +48,27 @@ def test_plugin_from_issue(mocker: MockerFixture) -> None: name="name", desc="desc", author="author", - homepage="https://www.baidu.com", + homepage="https://v2.nonebot.dev", tags=[{"label": "test", "color": "#ffffff"}], is_official=False, ) + calls = [ + mocker.call("https://pypi.org/pypi/project_link/json"), + mocker.call("https://v2.nonebot.dev"), + ] + mock_requests.assert_has_calls(calls) def test_plugin_from_issue_trailing_whitespace(mocker: MockerFixture) -> None: """测试末尾如果有空格的情况""" + mock_requests = mocker.patch("requests.get", side_effect=mocked_requests_get) mock_issue: Issue = mocker.MagicMock() mock_issue.body = generate_issue_body( module_name="module_name ", project_link="project_link ", name="name ", desc="desc ", - homepage="https://www.baidu.com ", + homepage="https://v2.nonebot.dev ", ) mock_issue.user.login = "author" @@ -82,25 +80,12 @@ def test_plugin_from_issue_trailing_whitespace(mocker: MockerFixture) -> None: name="name", desc="desc", author="author", - homepage="https://www.baidu.com", + homepage="https://v2.nonebot.dev", tags=[{"label": "test", "color": "#ffffff"}], is_official=False, ) -def mocked_requests_get(url: str): - class MockResponse: - def __init__(self, status_code: int): - self.status_code = status_code - - if url == "https://pypi.org/pypi/project_link/json": - return MockResponse(200) - if url == "https://v2.nonebot.dev": - return MockResponse(200) - - return MockResponse(404) - - def test_plugin_info_validation_success(mocker: MockerFixture) -> None: """测试验证成功的情况""" mock_requests = mocker.patch("requests.get", side_effect=mocked_requests_get) @@ -116,10 +101,9 @@ def test_plugin_info_validation_success(mocker: MockerFixture) -> None: is_official=False, ) - assert info.is_valid assert ( info.validation_message - == """> Plugin: name\n\n**✅ All tests passed, you are ready to go!**\n\n
    Report Detail
  • ✅ Project homepage returns 200.
  • ✅ Package project_link is available on PyPI.
  • """ + == """> Plugin: name\n\n**✅ 所有测试通过,一切准备就绪!**\n\n
    测试详情
  • ✅ 标签: test-#ffffff
  • ✅ 项目 主页 返回状态码 200.
  • ✅ 包 project_link 已发布至 PyPI
  • """ ) calls = [ @@ -132,24 +116,24 @@ def test_plugin_info_validation_success(mocker: MockerFixture) -> None: def test_plugin_info_validation_failed(mocker: MockerFixture) -> None: """测试验证失败的情况""" mock_requests = mocker.patch("requests.get", side_effect=mocked_requests_get) - - info = PluginPublishInfo( - module_name="module_name", + mock_issue: Issue = mocker.MagicMock() + mock_issue.body = generate_issue_body( project_link="project_link_failed", - name="name", - desc="desc", - author="author", homepage="https://www.baidu.com", - tags=[{"label": "test", "color": "#ffffff"}], - is_official=False, + tags=[ + {"label": "test", "color": "#ffffff"}, + {"label": "testtoolong", "color": "#fffffff"}, + ], ) + mock_issue.user.login = "author" + + with pytest.raises(MyValidationError) as e: + info = PluginPublishInfo.from_issue(mock_issue) - assert not info.is_valid assert ( - info.validation_message - == """> Plugin: name\n\n**⚠️ We have found following problem(s) in pre-publish progress:**\n
  • ⚠️ Project homepage returns 404.
    Please make sure that your project has a publicly visible homepage.
  • ⚠️ Package project_link_failed is not available on PyPI.
    Please publish your package to PyPI.
  • """ + e.value.message + == """> Plugin: name\n\n**⚠️ 在发布检查过程中,我们发现以下问题:**\n
  • ⚠️ 包 project_link_failed 未发布至 PyPI。
    请将您的包发布至 PyPI。
  • ⚠️ 项目 主页 返回状态码 404。
    请确保您的项目主页可访问。
  • 第 2 个标签名称不能超过 10 个字符
  • 第 2 个标签颜色不符合十六进制颜色码规则
  • """ ) - calls = [ mocker.call("https://pypi.org/pypi/project_link_failed/json"), mocker.call("https://www.baidu.com"), @@ -160,24 +144,19 @@ def test_plugin_info_validation_failed(mocker: MockerFixture) -> None: def test_plugin_info_validation_partial_failed(mocker: MockerFixture) -> None: """测试验证一部分失败的情况""" mock_requests = mocker.patch("requests.get", side_effect=mocked_requests_get) - - info = PluginPublishInfo( - module_name="module_name", - project_link="project_link", - name="name", - desc="desc", - author="author", + mock_issue: Issue = mocker.MagicMock() + mock_issue.body = generate_issue_body( homepage="https://www.baidu.com", - tags=[{"label": "test", "color": "#ffffff"}], - is_official=False, ) + mock_issue.user.login = "author" + + with pytest.raises(MyValidationError) as e: + info = PluginPublishInfo.from_issue(mock_issue) - assert not info.is_valid assert ( - info.validation_message - == """> Plugin: name\n\n**⚠️ We have found following problem(s) in pre-publish progress:**\n
  • ⚠️ Project homepage returns 404.
    Please make sure that your project has a publicly visible homepage.
  • \n
    Report Detail
  • ✅ Package project_link is available on PyPI.
  • """ + e.value.message + == """> Plugin: name\n\n**⚠️ 在发布检查过程中,我们发现以下问题:**\n
  • ⚠️ 项目 主页 返回状态码 404。
    请确保您的项目主页可访问。
  • \n
    测试详情
  • ✅ 标签: test-#ffffff
  • ✅ 包 project_link 已发布至 PyPI
  • """ ) - calls = [ mocker.call("https://pypi.org/pypi/project_link/json"), mocker.call("https://www.baidu.com"), diff --git a/tests/models/test_tags.py b/tests/models/test_tags.py new file mode 100644 index 00000000..5d44f2f3 --- /dev/null +++ b/tests/models/test_tags.py @@ -0,0 +1,93 @@ +# type: ignore +import json +from collections import OrderedDict + +import pytest +from github.Issue import Issue +from pydantic import ValidationError +from pytest_mock import MockerFixture + +from src.models import AdapterPublishInfo, MyValidationError + + +def generate_issue_body( + name: str = "name", + desc: str = "desc", + module_name: str = "module_name", + project_link: str = "project_link", + homepage: str = "https://v2.nonebot.dev", + tags: list = [{"label": "test", "color": "#ffffff"}], +): + return f"""**协议名称:**\n\n{name}\n\n**协议功能:**\n\n{desc}\n\n**PyPI 项目名:**\n\n{project_link}\n\n**协议 import 包名:**\n\n{module_name}\n\n**协议项目仓库/主页链接:**\n\n{homepage}\n\n**标签:**\n\n{json.dumps(tags)}""" + + +def mocked_requests_get(url: str): + class MockResponse: + def __init__(self, status_code: int): + self.status_code = status_code + + if url == "https://pypi.org/pypi/project_link/json": + return MockResponse(200) + if url == "https://v2.nonebot.dev": + return MockResponse(200) + + return MockResponse(404) + + +def test_adapter_tags_color_invalid(mocker: MockerFixture) -> None: + """测试标签颜色不正确的情况""" + mock_requests = mocker.patch("requests.get", side_effect=mocked_requests_get) + + with pytest.raises(ValidationError) as e: + info = AdapterPublishInfo( + module_name="module_name", + project_link="project_link", + name="name", + desc="desc", + author="author", + homepage="https://v2.nonebot.dev", + tags=[{"label": "test", "color": "#adbcdef"}], + is_official=False, + ) + assert "标签颜色不符合十六进制颜色码规则" in str(e.value) + + +def test_adapter_tags_label_invalid(mocker: MockerFixture) -> None: + """测试标签名称不正确的情况""" + mock_requests = mocker.patch("requests.get", side_effect=mocked_requests_get) + + with pytest.raises(ValidationError) as e: + info = AdapterPublishInfo( + module_name="module_name", + project_link="project_link", + name="name", + desc="desc", + author="author", + homepage="https://v2.nonebot.dev", + tags=[{"label": "12345678901", "color": "#adbcde"}], + is_official=False, + ) + assert "标签名称不能超过 10 个字符" in str(e.value) + + +def test_adapter_tags_number_invalid(mocker: MockerFixture) -> None: + """测试标签数量不正确的情况""" + mock_requests = mocker.patch("requests.get", side_effect=mocked_requests_get) + + with pytest.raises(ValidationError) as e: + info = AdapterPublishInfo( + module_name="module_name", + project_link="project_link", + name="name", + desc="desc", + author="author", + homepage="https://v2.nonebot.dev", + tags=[ + {"label": "1", "color": "#ffffff"}, + {"label": "2", "color": "#ffffff"}, + {"label": "3", "color": "#ffffff"}, + {"label": "4", "color": "#ffffff"}, + ], + is_official=False, + ) + assert "标签数量不能超过 3 个" in str(e.value) diff --git a/tests/process/test_issues.py b/tests/process/test_issues.py index db223fd6..dbf5f96d 100644 --- a/tests/process/test_issues.py +++ b/tests/process/test_issues.py @@ -119,5 +119,5 @@ def test_process_issues(mocker: MockerFixture, tmp_path: Path) -> None: # 检查是否创建了评论 mock_repo.get_issue().create_comment.assert_called_with( - """# 📃 Publish Check Result\n\n> Bot: test\n\n**✅ All tests passed, you are ready to go!**\n\n
    Report Detail
  • ✅ Project homepage returns 200.
  • \n\n---\n\n💪 Powered by NoneBot2 Publish Bot\n""" + """# 📃 商店发布检查结果\n\n> Bot: test\n\n**✅ 所有测试通过,一切准备就绪!**\n\n
    测试详情
  • ✅ 标签: test-#ffffff
  • ✅ 项目 主页 返回状态码 200.
  • \n\n---\n\n💪 Powered by NoneBot2 Publish Bot\n""" ) diff --git a/tests/utils/test_comment_issue.py b/tests/utils/test_comment_issue.py index 9c396822..bc77e263 100644 --- a/tests/utils/test_comment_issue.py +++ b/tests/utils/test_comment_issue.py @@ -15,19 +15,19 @@ def test_comment_issue(mocker: MockerFixture): mock_comment.edit.assert_not_called() mock_issue.create_comment.assert_called_once_with( - "# 📃 Publish Check Result\n\ntest\n\n---\n\n💪 Powered by NoneBot2 Publish Bot\n" + "# 📃 商店发布检查结果\n\ntest\n\n---\n\n💪 Powered by NoneBot2 Publish Bot\n" ) def test_comment_issue_reuse(mocker: MockerFixture): mock_issue: Issue = mocker.MagicMock() # type: ignore mock_comment = mocker.MagicMock() - mock_comment.body = "# 📃 Publish Check Result" + mock_comment.body = "# 📃 商店发布检查结果" mock_issue.get_comments.return_value = [mock_comment] comment_issue(mock_issue, "test") mock_issue.create_comment.assert_not_called() mock_comment.edit.assert_called_once_with( - "# 📃 Publish Check Result\n\ntest\n\n---\n\n♻️ This comment has been updated with the latest result.\n\n💪 Powered by NoneBot2 Publish Bot\n" + "# 📃 商店发布检查结果\n\ntest\n\n---\n\n♻️ 评论已更新至最新检查结果\n\n💪 Powered by NoneBot2 Publish Bot\n" )