imbi-plugin-github 2.38.0__tar.gz → 2.39.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/PKG-INFO +2 -2
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/pyproject.toml +2 -2
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/src/imbi/plugins/github/doctor.py +143 -6
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/src/imbi/plugins/github/plugin.py +3 -2
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/tests/test_doctor.py +162 -2
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/.gitignore +0 -0
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/README.md +0 -0
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/moon.yml +0 -0
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/src/imbi/plugins/github/README.md +0 -0
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/src/imbi/plugins/github/__init__.py +0 -0
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/src/imbi/plugins/github/_app_auth.py +0 -0
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/src/imbi/plugins/github/_hosts.py +0 -0
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/src/imbi/plugins/github/_repos.py +0 -0
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/src/imbi/plugins/github/commits.py +0 -0
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/src/imbi/plugins/github/deployment.py +0 -0
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/src/imbi/plugins/github/identity.py +0 -0
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/src/imbi/plugins/github/lifecycle.py +0 -0
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/src/imbi/plugins/github/pull_requests.py +0 -0
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/src/imbi/plugins/github/py.typed +0 -0
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/tests/__init__.py +0 -0
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/tests/test_app_auth_scopes.py +0 -0
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/tests/test_commits.py +0 -0
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/tests/test_deployment.py +0 -0
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/tests/test_hosts.py +0 -0
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/tests/test_identity.py +0 -0
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/tests/test_lifecycle.py +0 -0
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/tests/test_pull_requests.py +0 -0
- {imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/tests/test_repos.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: imbi-plugin-github
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.39.0
|
|
4
4
|
Summary: GitHub identity plugin for Imbi (github.com / GHEC / GHES)
|
|
5
5
|
Author-email: "Gavin M. Roy" <gavinr@aweber.com>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.14
|
|
13
13
|
Requires-Python: >=3.14
|
|
14
14
|
Requires-Dist: httpx>=0.27
|
|
15
|
-
Requires-Dist: imbi-common[databases]==2.
|
|
15
|
+
Requires-Dist: imbi-common[databases]==2.39.0
|
|
16
16
|
Requires-Dist: pydantic>=2
|
|
17
17
|
Requires-Dist: pyjwt[crypto]>=2.8
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "imbi-plugin-github"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.39.0"
|
|
4
4
|
description = "GitHub identity plugin for Imbi (github.com / GHEC / GHES)"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.14"
|
|
@@ -18,7 +18,7 @@ classifiers = [
|
|
|
18
18
|
]
|
|
19
19
|
dependencies = [
|
|
20
20
|
"httpx>=0.27",
|
|
21
|
-
"imbi-common[databases]==2.
|
|
21
|
+
"imbi-common[databases]==2.39.0",
|
|
22
22
|
"pydantic>=2",
|
|
23
23
|
"pyjwt[crypto]>=2.8",
|
|
24
24
|
]
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Performs richer, GitHub-aware validation than the generic exists-in doctor:
|
|
4
4
|
identifier is an integer, canonical URL follows the /repositories/{id} shape
|
|
5
|
-
that the lifecycle plugin writes, dashboard link matches the API html_url,
|
|
6
|
-
the legacy github-repository link matches as well
|
|
5
|
+
that the lifecycle plugin writes, dashboard link matches the API html_url,
|
|
6
|
+
the legacy github-repository link matches as well, and the repository
|
|
7
|
+
``homepage`` points back at the project's Imbi page.
|
|
7
8
|
|
|
8
9
|
A single analysis capability covers github.com, GHEC tenants, and GHES
|
|
9
10
|
appliances: the GitHub host is resolved from the Integration's ``flavor`` +
|
|
@@ -53,8 +54,14 @@ _REPO_ID_RE = re.compile(r'.*/repositories/(\d+)$')
|
|
|
53
54
|
# edge-shaped discrepancy (identifier, canonical URL shape, dashboard
|
|
54
55
|
# link) is repaired by one ``ServiceWriteback`` so they share an id; the
|
|
55
56
|
# legacy ``github-repository`` link is a separate ``LinkWriteback``.
|
|
57
|
+
# The ``homepage`` repair is the one that writes *to* GitHub: it PATCHes
|
|
58
|
+
# the repository so its homepage is the project's Imbi page.
|
|
56
59
|
_REPAIR_EDGE = 'repair-edge'
|
|
57
60
|
_REPAIR_GITHUB_LINK = 'repair-github-link'
|
|
61
|
+
_REPAIR_HOMEPAGE = 'repair-homepage'
|
|
62
|
+
_REMEDIATION_IDS = frozenset(
|
|
63
|
+
{_REPAIR_EDGE, _REPAIR_GITHUB_LINK, _REPAIR_HOMEPAGE}
|
|
64
|
+
)
|
|
58
65
|
|
|
59
66
|
|
|
60
67
|
async def _raise_on_401(response: httpx.Response) -> None:
|
|
@@ -554,6 +561,8 @@ class GitHubDoctor(AnalysisCapability):
|
|
|
554
561
|
)
|
|
555
562
|
)
|
|
556
563
|
|
|
564
|
+
results.append(_homepage_item(ctx, body))
|
|
565
|
+
|
|
557
566
|
return results
|
|
558
567
|
|
|
559
568
|
async def remediate( # noqa: C901 — flat sequence of guard clauses
|
|
@@ -573,7 +582,7 @@ class GitHubDoctor(AnalysisCapability):
|
|
|
573
582
|
``PluginAuthenticationFailed`` so the host can refresh the actor's
|
|
574
583
|
identity and retry once.
|
|
575
584
|
"""
|
|
576
|
-
if remediation_id not in
|
|
585
|
+
if remediation_id not in _REMEDIATION_IDS:
|
|
577
586
|
return await super().remediate(ctx, credentials, remediation_id)
|
|
578
587
|
|
|
579
588
|
try:
|
|
@@ -593,9 +602,9 @@ class GitHubDoctor(AnalysisCapability):
|
|
|
593
602
|
connection = _find_connection(ctx.service_connections, slug)
|
|
594
603
|
# ``_REPAIR_EDGE`` creates a missing edge from GitHub, so a
|
|
595
604
|
# missing connection is only fatal for the github-repository
|
|
596
|
-
# link
|
|
597
|
-
# offered once the edge is present).
|
|
598
|
-
if connection is None and remediation_id
|
|
605
|
+
# link and homepage repairs (which never need the edge but are
|
|
606
|
+
# only ever offered once the edge is present).
|
|
607
|
+
if connection is None and remediation_id != _REPAIR_EDGE:
|
|
599
608
|
return RemediationResult(
|
|
600
609
|
status='failed',
|
|
601
610
|
message=(
|
|
@@ -661,6 +670,9 @@ class GitHubDoctor(AnalysisCapability):
|
|
|
661
670
|
message='GitHub response did not include an html_url.',
|
|
662
671
|
)
|
|
663
672
|
|
|
673
|
+
if remediation_id == _REPAIR_HOMEPAGE:
|
|
674
|
+
return await _repair_homepage(ctx, body, api_base, headers)
|
|
675
|
+
|
|
664
676
|
if remediation_id == _REPAIR_GITHUB_LINK:
|
|
665
677
|
if ctx.project_links.get('github-repository') == html_url:
|
|
666
678
|
return RemediationResult(
|
|
@@ -769,4 +781,129 @@ def _body_unavailable_items() -> list[AnalysisResultItem]:
|
|
|
769
781
|
'warn',
|
|
770
782
|
'Cannot verify github-repository link: repository fetch failed.',
|
|
771
783
|
),
|
|
784
|
+
_item(
|
|
785
|
+
'homepage-url-match',
|
|
786
|
+
'Homepage URL match',
|
|
787
|
+
'warn',
|
|
788
|
+
'Cannot verify the repository homepage: repository fetch failed.',
|
|
789
|
+
),
|
|
772
790
|
]
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def _repo_homepage(body: dict[str, typing.Any]) -> str:
|
|
794
|
+
"""Return the repository ``homepage``, normalising unset to ``''``.
|
|
795
|
+
|
|
796
|
+
GitHub reports an unset homepage as ``null`` on some flavors and as
|
|
797
|
+
the empty string on others; both mean "not set".
|
|
798
|
+
"""
|
|
799
|
+
homepage = body.get('homepage')
|
|
800
|
+
return homepage if isinstance(homepage, str) else ''
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def _homepage_item(
|
|
804
|
+
ctx: PluginContext, body: dict[str, typing.Any]
|
|
805
|
+
) -> AnalysisResultItem:
|
|
806
|
+
"""Check that the repository ``homepage`` is the project's Imbi page.
|
|
807
|
+
|
|
808
|
+
The expected value is ``ctx.project_ui_url``, the same deep link the
|
|
809
|
+
lifecycle plugin writes on create / update. It is ``None`` when the
|
|
810
|
+
API has no ``IMBI_UI_URL`` configured, in which case there is
|
|
811
|
+
nothing to compare against and no repair to offer.
|
|
812
|
+
"""
|
|
813
|
+
expected = ctx.project_ui_url
|
|
814
|
+
actual = _repo_homepage(body)
|
|
815
|
+
if expected is None:
|
|
816
|
+
return _item(
|
|
817
|
+
'homepage-url-match',
|
|
818
|
+
'Homepage URL match',
|
|
819
|
+
'warn',
|
|
820
|
+
'Cannot verify the repository homepage: IMBI_UI_URL is not '
|
|
821
|
+
'configured on the API, so the project has no canonical '
|
|
822
|
+
'Imbi page URL.',
|
|
823
|
+
)
|
|
824
|
+
if actual == expected:
|
|
825
|
+
return _item(
|
|
826
|
+
'homepage-url-match',
|
|
827
|
+
'Homepage URL match',
|
|
828
|
+
'pass',
|
|
829
|
+
f"Repository homepage {actual!r} matches the project's Imbi page.",
|
|
830
|
+
)
|
|
831
|
+
offer = RemediationOffer(
|
|
832
|
+
id=_REPAIR_HOMEPAGE,
|
|
833
|
+
label="Set the repository homepage to the project's Imbi page",
|
|
834
|
+
)
|
|
835
|
+
if not actual:
|
|
836
|
+
return _item(
|
|
837
|
+
'homepage-url-match',
|
|
838
|
+
'Homepage URL match',
|
|
839
|
+
'warn',
|
|
840
|
+
'The repository has no homepage set. Use the Fix action to '
|
|
841
|
+
f"point it at the project's Imbi page {expected!r}.",
|
|
842
|
+
offer,
|
|
843
|
+
)
|
|
844
|
+
return _item(
|
|
845
|
+
'homepage-url-match',
|
|
846
|
+
'Homepage URL match',
|
|
847
|
+
'fail',
|
|
848
|
+
f"Repository homepage {actual!r} does not match the project's "
|
|
849
|
+
f'Imbi page {expected!r}. Use the Fix action to update it.',
|
|
850
|
+
offer,
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
async def _repair_homepage(
|
|
855
|
+
ctx: PluginContext,
|
|
856
|
+
body: dict[str, typing.Any],
|
|
857
|
+
api_base: str,
|
|
858
|
+
headers: dict[str, str],
|
|
859
|
+
) -> RemediationResult:
|
|
860
|
+
"""PATCH the repository ``homepage`` to the project's Imbi page.
|
|
861
|
+
|
|
862
|
+
Unlike the edge / link repairs, which write back into Imbi, this
|
|
863
|
+
one writes to GitHub -- the same ``PATCH /repos/{owner}/{repo}``
|
|
864
|
+
the lifecycle plugin uses on update. ``body`` is the freshly
|
|
865
|
+
fetched repository, so ``full_name`` reflects any rename or
|
|
866
|
+
transfer. Idempotent: ``noop`` when GitHub already matches.
|
|
867
|
+
"""
|
|
868
|
+
expected = ctx.project_ui_url
|
|
869
|
+
if expected is None:
|
|
870
|
+
return RemediationResult(
|
|
871
|
+
status='failed',
|
|
872
|
+
message=(
|
|
873
|
+
'IMBI_UI_URL is not configured on the API, so there is no '
|
|
874
|
+
'Imbi page URL to write as the repository homepage.'
|
|
875
|
+
),
|
|
876
|
+
)
|
|
877
|
+
if _repo_homepage(body) == expected:
|
|
878
|
+
return RemediationResult(
|
|
879
|
+
status='noop',
|
|
880
|
+
message='Repository homepage already matches the Imbi page.',
|
|
881
|
+
)
|
|
882
|
+
full_name = body.get('full_name')
|
|
883
|
+
if not isinstance(full_name, str) or not full_name:
|
|
884
|
+
return RemediationResult(
|
|
885
|
+
status='failed',
|
|
886
|
+
message='GitHub response did not include a full_name.',
|
|
887
|
+
)
|
|
888
|
+
patch_url = f'{api_base}/repos/{full_name}'
|
|
889
|
+
try:
|
|
890
|
+
async with httpx.AsyncClient(
|
|
891
|
+
headers=headers,
|
|
892
|
+
timeout=_TIMEOUT,
|
|
893
|
+
event_hooks={'response': [_raise_on_401]},
|
|
894
|
+
) as client:
|
|
895
|
+
resp = await client.patch(patch_url, json={'homepage': expected})
|
|
896
|
+
except httpx.TransportError as exc:
|
|
897
|
+
return RemediationResult(
|
|
898
|
+
status='failed',
|
|
899
|
+
message=f'Transport error patching {patch_url!r}: {exc}',
|
|
900
|
+
)
|
|
901
|
+
if not resp.is_success:
|
|
902
|
+
return RemediationResult(
|
|
903
|
+
status='failed',
|
|
904
|
+
message=f'HTTP {resp.status_code} patching {patch_url!r}.',
|
|
905
|
+
)
|
|
906
|
+
return RemediationResult(
|
|
907
|
+
status='fixed',
|
|
908
|
+
message=f'Set the repository homepage to {expected}.',
|
|
909
|
+
)
|
|
@@ -410,8 +410,9 @@ class GitHubPlugin(Plugin):
|
|
|
410
410
|
description=(
|
|
411
411
|
'Validate the GitHub repository link (EXISTS_IN edge) '
|
|
412
412
|
'against the live GitHub API — identifier, canonical '
|
|
413
|
-
'URL shape,
|
|
414
|
-
|
|
413
|
+
'URL shape, dashboard / github-repository links, and '
|
|
414
|
+
"the repository homepage pointing at the project's "
|
|
415
|
+
'Imbi page — and offer one-click repairs.'
|
|
415
416
|
),
|
|
416
417
|
handler=GitHubDoctor,
|
|
417
418
|
),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Tests for the GitHub-native project-doctor analysis plugin."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import unittest
|
|
4
5
|
|
|
5
6
|
import httpx
|
|
@@ -20,6 +21,7 @@ from imbi.plugins.github import _app_auth
|
|
|
20
21
|
from imbi.plugins.github.doctor import (
|
|
21
22
|
_REPAIR_EDGE,
|
|
22
23
|
_REPAIR_GITHUB_LINK,
|
|
24
|
+
_REPAIR_HOMEPAGE,
|
|
23
25
|
GitHubDoctor,
|
|
24
26
|
)
|
|
25
27
|
|
|
@@ -27,7 +29,15 @@ _HOST = 'aweber.ghe.com'
|
|
|
27
29
|
_API_BASE = 'https://api.aweber.ghe.com'
|
|
28
30
|
_CANONICAL = f'{_API_BASE}/repositories/134741'
|
|
29
31
|
_DASHBOARD = f'https://{_HOST}/aweber/demo'
|
|
30
|
-
|
|
32
|
+
_UI_URL = 'https://imbi.example.com/projects/p'
|
|
33
|
+
_REPO_PAYLOAD = {
|
|
34
|
+
'id': 134741,
|
|
35
|
+
'html_url': _DASHBOARD,
|
|
36
|
+
'name': 'demo',
|
|
37
|
+
'full_name': 'aweber/demo',
|
|
38
|
+
'homepage': _UI_URL,
|
|
39
|
+
}
|
|
40
|
+
_PATCH_URL = f'{_API_BASE}/repos/aweber/demo'
|
|
31
41
|
|
|
32
42
|
_TPS_SLUG = 'aweber-github'
|
|
33
43
|
_CREDS = {'access_token': 'gho_test'}
|
|
@@ -53,6 +63,7 @@ def _ctx(
|
|
|
53
63
|
connections: list[ServiceConnection] | None = None,
|
|
54
64
|
links: dict[str, str] | None = None,
|
|
55
65
|
options: dict[str, str] | None = None,
|
|
66
|
+
project_ui_url: str | None = _UI_URL,
|
|
56
67
|
) -> PluginContext:
|
|
57
68
|
if connections is None:
|
|
58
69
|
connections = [
|
|
@@ -79,6 +90,7 @@ def _ctx(
|
|
|
79
90
|
),
|
|
80
91
|
service_connections=connections,
|
|
81
92
|
project_links=links if links is not None else default_links,
|
|
93
|
+
project_ui_url=project_ui_url,
|
|
82
94
|
)
|
|
83
95
|
|
|
84
96
|
|
|
@@ -143,7 +155,7 @@ class AnalyzeTestCase(unittest.IsolatedAsyncioTestCase):
|
|
|
143
155
|
plugin = GitHubDoctor()
|
|
144
156
|
results = await plugin.analyze(_ctx(), _CREDS)
|
|
145
157
|
by = _by_slug(results)
|
|
146
|
-
self.assertEqual(len(results),
|
|
158
|
+
self.assertEqual(len(results), 9)
|
|
147
159
|
self.assertEqual(by['exists-in'].status, 'pass')
|
|
148
160
|
self.assertEqual(by['identifier-verified'].status, 'pass')
|
|
149
161
|
self.assertEqual(by['canonical-url'].status, 'pass')
|
|
@@ -152,6 +164,7 @@ class AnalyzeTestCase(unittest.IsolatedAsyncioTestCase):
|
|
|
152
164
|
self.assertEqual(by['canonical-url-shape'].status, 'pass')
|
|
153
165
|
self.assertEqual(by['dashboard-url-match'].status, 'pass')
|
|
154
166
|
self.assertEqual(by['github-repository-link-match'].status, 'pass')
|
|
167
|
+
self.assertEqual(by['homepage-url-match'].status, 'pass')
|
|
155
168
|
|
|
156
169
|
@respx.mock
|
|
157
170
|
async def test_identifier_not_integer(self) -> None:
|
|
@@ -324,6 +337,52 @@ class AnalyzeTestCase(unittest.IsolatedAsyncioTestCase):
|
|
|
324
337
|
'pass',
|
|
325
338
|
)
|
|
326
339
|
|
|
340
|
+
@respx.mock
|
|
341
|
+
async def test_homepage_mismatch(self) -> None:
|
|
342
|
+
respx.get(_CANONICAL).mock(
|
|
343
|
+
return_value=httpx.Response(
|
|
344
|
+
200,
|
|
345
|
+
json={**_REPO_PAYLOAD, 'homepage': 'https://example.com'},
|
|
346
|
+
)
|
|
347
|
+
)
|
|
348
|
+
results = await GitHubDoctor().analyze(_ctx(), _CREDS)
|
|
349
|
+
item = _by_slug(results)['homepage-url-match']
|
|
350
|
+
self.assertEqual(item.status, 'fail')
|
|
351
|
+
self.assertIn(_UI_URL, item.description)
|
|
352
|
+
assert item.remediation is not None
|
|
353
|
+
self.assertEqual(item.remediation.id, _REPAIR_HOMEPAGE)
|
|
354
|
+
|
|
355
|
+
@respx.mock
|
|
356
|
+
async def test_homepage_unset(self) -> None:
|
|
357
|
+
# GitHub reports an unset homepage as null (or '' on some
|
|
358
|
+
# flavors); both are a soft finding with the repair offered.
|
|
359
|
+
for unset in (None, ''):
|
|
360
|
+
with self.subTest(homepage=unset):
|
|
361
|
+
respx.get(_CANONICAL).mock(
|
|
362
|
+
return_value=httpx.Response(
|
|
363
|
+
200, json={**_REPO_PAYLOAD, 'homepage': unset}
|
|
364
|
+
)
|
|
365
|
+
)
|
|
366
|
+
results = await GitHubDoctor().analyze(_ctx(), _CREDS)
|
|
367
|
+
item = _by_slug(results)['homepage-url-match']
|
|
368
|
+
self.assertEqual(item.status, 'warn')
|
|
369
|
+
assert item.remediation is not None
|
|
370
|
+
self.assertEqual(item.remediation.id, _REPAIR_HOMEPAGE)
|
|
371
|
+
|
|
372
|
+
@respx.mock
|
|
373
|
+
async def test_homepage_unverifiable_without_ui_url(self) -> None:
|
|
374
|
+
# No IMBI_UI_URL on the API: nothing to compare against, and no
|
|
375
|
+
# repair is offered because there is nothing to write.
|
|
376
|
+
respx.get(_CANONICAL).mock(
|
|
377
|
+
return_value=httpx.Response(200, json=_REPO_PAYLOAD)
|
|
378
|
+
)
|
|
379
|
+
results = await GitHubDoctor().analyze(
|
|
380
|
+
_ctx(project_ui_url=None), _CREDS
|
|
381
|
+
)
|
|
382
|
+
item = _by_slug(results)['homepage-url-match']
|
|
383
|
+
self.assertEqual(item.status, 'warn')
|
|
384
|
+
self.assertIsNone(item.remediation)
|
|
385
|
+
|
|
327
386
|
@respx.mock
|
|
328
387
|
async def test_canonical_fetch_fails_401_no_token(self) -> None:
|
|
329
388
|
respx.get(_CANONICAL).mock(return_value=httpx.Response(401))
|
|
@@ -614,6 +673,107 @@ class RemediateTestCase(unittest.IsolatedAsyncioTestCase):
|
|
|
614
673
|
self.assertEqual(result.status, 'noop')
|
|
615
674
|
self.assertIsNone(ctx.link_writeback)
|
|
616
675
|
|
|
676
|
+
@respx.mock
|
|
677
|
+
async def test_homepage_repair_patches_github(self) -> None:
|
|
678
|
+
respx.get(_CANONICAL).mock(
|
|
679
|
+
return_value=httpx.Response(
|
|
680
|
+
200, json={**_REPO_PAYLOAD, 'homepage': ''}
|
|
681
|
+
)
|
|
682
|
+
)
|
|
683
|
+
patch = respx.patch(_PATCH_URL).mock(
|
|
684
|
+
return_value=httpx.Response(200, json=_REPO_PAYLOAD)
|
|
685
|
+
)
|
|
686
|
+
ctx = _ctx()
|
|
687
|
+
result = await GitHubDoctor().remediate(ctx, _CREDS, _REPAIR_HOMEPAGE)
|
|
688
|
+
self.assertEqual(result.status, 'fixed')
|
|
689
|
+
self.assertTrue(patch.called)
|
|
690
|
+
self.assertEqual(
|
|
691
|
+
json.loads(patch.calls.last.request.content),
|
|
692
|
+
{'homepage': _UI_URL},
|
|
693
|
+
)
|
|
694
|
+
# Writes to GitHub, not back into Imbi.
|
|
695
|
+
self.assertIsNone(ctx.link_writeback)
|
|
696
|
+
self.assertIsNone(ctx.service_writeback)
|
|
697
|
+
|
|
698
|
+
@respx.mock
|
|
699
|
+
async def test_homepage_repair_noop_when_correct(self) -> None:
|
|
700
|
+
respx.get(_CANONICAL).mock(
|
|
701
|
+
return_value=httpx.Response(200, json=_REPO_PAYLOAD)
|
|
702
|
+
)
|
|
703
|
+
patch = respx.patch(_PATCH_URL)
|
|
704
|
+
result = await GitHubDoctor().remediate(
|
|
705
|
+
_ctx(), _CREDS, _REPAIR_HOMEPAGE
|
|
706
|
+
)
|
|
707
|
+
self.assertEqual(result.status, 'noop')
|
|
708
|
+
self.assertFalse(patch.called)
|
|
709
|
+
|
|
710
|
+
async def test_homepage_repair_no_connection_failed(self) -> None:
|
|
711
|
+
result = await GitHubDoctor().remediate(
|
|
712
|
+
_ctx(connections=[]), _CREDS, _REPAIR_HOMEPAGE
|
|
713
|
+
)
|
|
714
|
+
self.assertEqual(result.status, 'failed')
|
|
715
|
+
|
|
716
|
+
@respx.mock
|
|
717
|
+
async def test_homepage_repair_without_ui_url_failed(self) -> None:
|
|
718
|
+
respx.get(_CANONICAL).mock(
|
|
719
|
+
return_value=httpx.Response(
|
|
720
|
+
200, json={**_REPO_PAYLOAD, 'homepage': ''}
|
|
721
|
+
)
|
|
722
|
+
)
|
|
723
|
+
result = await GitHubDoctor().remediate(
|
|
724
|
+
_ctx(project_ui_url=None), _CREDS, _REPAIR_HOMEPAGE
|
|
725
|
+
)
|
|
726
|
+
self.assertEqual(result.status, 'failed')
|
|
727
|
+
|
|
728
|
+
@respx.mock
|
|
729
|
+
async def test_homepage_repair_missing_full_name_failed(self) -> None:
|
|
730
|
+
payload = {**_REPO_PAYLOAD, 'homepage': ''}
|
|
731
|
+
del payload['full_name']
|
|
732
|
+
respx.get(_CANONICAL).mock(
|
|
733
|
+
return_value=httpx.Response(200, json=payload)
|
|
734
|
+
)
|
|
735
|
+
result = await GitHubDoctor().remediate(
|
|
736
|
+
_ctx(), _CREDS, _REPAIR_HOMEPAGE
|
|
737
|
+
)
|
|
738
|
+
self.assertEqual(result.status, 'failed')
|
|
739
|
+
|
|
740
|
+
@respx.mock
|
|
741
|
+
async def test_homepage_repair_patch_rejected_failed(self) -> None:
|
|
742
|
+
respx.get(_CANONICAL).mock(
|
|
743
|
+
return_value=httpx.Response(
|
|
744
|
+
200, json={**_REPO_PAYLOAD, 'homepage': ''}
|
|
745
|
+
)
|
|
746
|
+
)
|
|
747
|
+
respx.patch(_PATCH_URL).mock(return_value=httpx.Response(403))
|
|
748
|
+
result = await GitHubDoctor().remediate(
|
|
749
|
+
_ctx(), _CREDS, _REPAIR_HOMEPAGE
|
|
750
|
+
)
|
|
751
|
+
self.assertEqual(result.status, 'failed')
|
|
752
|
+
|
|
753
|
+
@respx.mock
|
|
754
|
+
async def test_homepage_repair_patch_401_propagates(self) -> None:
|
|
755
|
+
respx.get(_CANONICAL).mock(
|
|
756
|
+
return_value=httpx.Response(
|
|
757
|
+
200, json={**_REPO_PAYLOAD, 'homepage': ''}
|
|
758
|
+
)
|
|
759
|
+
)
|
|
760
|
+
respx.patch(_PATCH_URL).mock(return_value=httpx.Response(401))
|
|
761
|
+
with self.assertRaises(PluginAuthenticationFailed):
|
|
762
|
+
await GitHubDoctor().remediate(_ctx(), _CREDS, _REPAIR_HOMEPAGE)
|
|
763
|
+
|
|
764
|
+
@respx.mock
|
|
765
|
+
async def test_homepage_repair_transport_error_failed(self) -> None:
|
|
766
|
+
respx.get(_CANONICAL).mock(
|
|
767
|
+
return_value=httpx.Response(
|
|
768
|
+
200, json={**_REPO_PAYLOAD, 'homepage': ''}
|
|
769
|
+
)
|
|
770
|
+
)
|
|
771
|
+
respx.patch(_PATCH_URL).mock(side_effect=httpx.ConnectError('down'))
|
|
772
|
+
result = await GitHubDoctor().remediate(
|
|
773
|
+
_ctx(), _CREDS, _REPAIR_HOMEPAGE
|
|
774
|
+
)
|
|
775
|
+
self.assertEqual(result.status, 'failed')
|
|
776
|
+
|
|
617
777
|
@respx.mock
|
|
618
778
|
async def test_401_propagates_for_identity_retry(self) -> None:
|
|
619
779
|
respx.get(_CANONICAL).mock(return_value=httpx.Response(401))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/src/imbi/plugins/github/_app_auth.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/src/imbi/plugins/github/deployment.py
RENAMED
|
File without changes
|
|
File without changes
|
{imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/src/imbi/plugins/github/lifecycle.py
RENAMED
|
File without changes
|
{imbi_plugin_github-2.38.0 → imbi_plugin_github-2.39.0}/src/imbi/plugins/github/pull_requests.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|