Skip to content
Open
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
27 changes: 24 additions & 3 deletions gittensor/cli/miner_commands/post.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
'rejected': '[red]✗[/red]',
'no_response': '[yellow]—[/yellow]',
}
_PAT_GRAPHQL_ACCESS_ERROR = (
'[red]PAT lacks GraphQL API access. Fine-grained PATs need "Public Repositories (read-only)" permission.[/red]'
)


@click.command()
Expand Down Expand Up @@ -214,9 +217,27 @@ def _validate_pat_locally(pat: str) -> str | None:
timeout=GITHUB_HTTP_TIMEOUT_SECONDS,
)
if gql_resp.status_code != 200:
err_console.print(
'[red]PAT lacks GraphQL API access. Fine-grained PATs need "Public Repositories (read-only)" permission.[/red]'
)
err_console.print(_PAT_GRAPHQL_ACCESS_ERROR)
return None

try:
gql_data = gql_resp.json()
except ValueError:
return None

if not isinstance(gql_data, dict):
err_console.print(_PAT_GRAPHQL_ACCESS_ERROR)
return None

errors = gql_data.get('errors')
if errors:
err_console.print(_PAT_GRAPHQL_ACCESS_ERROR)
return None

response_data = gql_data.get('data')
viewer = response_data.get('viewer') if isinstance(response_data, dict) else None
if viewer is None:
err_console.print(_PAT_GRAPHQL_ACCESS_ERROR)
return None

return login
Expand Down
47 changes: 46 additions & 1 deletion tests/cli/test_miner_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import json
from types import SimpleNamespace
from unittest.mock import patch
from unittest.mock import MagicMock, patch

import pytest
from click.testing import CliRunner
Expand All @@ -18,6 +18,7 @@
_pat_post_row_category,
_require_validator_axons,
)
from gittensor.cli.miner_commands.post import _validate_pat_locally


def _fake_metagraph(rows: list[tuple[float, bool, float]]):
Expand All @@ -31,11 +32,55 @@ def _fake_metagraph(rows: list[tuple[float, bool, float]]):
)


def _mock_http_response(status_code: int = 200, payload=None, json_error=None):
response = MagicMock()
response.status_code = status_code
if json_error is not None:
response.json.side_effect = json_error
else:
response.json.return_value = payload if payload is not None else {}
return response


@pytest.fixture
def runner():
return CliRunner()


class TestValidatePatLocally:
@patch('gittensor.cli.miner_commands.post.requests.post')
@patch('gittensor.cli.miner_commands.post.requests.get')
def test_valid_viewer_returns_login(self, mock_get, mock_post):
mock_get.return_value = _mock_http_response(200, {'login': 'octocat'})
mock_post.return_value = _mock_http_response(200, {'data': {'viewer': {'login': 'octocat'}}})

assert _validate_pat_locally('ghp_valid') == 'octocat'

@pytest.mark.parametrize(
'payload',
[
{'data': {'viewer': None}},
{'data': None},
{'errors': [{'message': 'Bad credentials'}]},
],
)
@patch('gittensor.cli.miner_commands.post.requests.post')
@patch('gittensor.cli.miner_commands.post.requests.get')
def test_graphql_body_rejections_return_none(self, mock_get, mock_post, payload):
mock_get.return_value = _mock_http_response(200, {'login': 'octocat'})
mock_post.return_value = _mock_http_response(200, payload)

assert _validate_pat_locally('ghp_scopeless') is None

@patch('gittensor.cli.miner_commands.post.requests.post')
@patch('gittensor.cli.miner_commands.post.requests.get')
def test_graphql_json_decode_error_returns_none(self, mock_get, mock_post):
mock_get.return_value = _mock_http_response(200, {'login': 'octocat'})
mock_post.return_value = _mock_http_response(200, json_error=ValueError('not json'))

assert _validate_pat_locally('ghp_bad_json') is None


class TestMinerPost:
@patch('gittensor.cli.miner_commands.post.click.prompt', return_value='ghp_fake')
@patch('gittensor.cli.miner_commands.post._validate_pat_locally', return_value=None)
Expand Down